Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be43913ae3 | ||
|
|
f9ef1d48a4 | ||
|
|
5be1365038 | ||
|
|
89413c17a4 | ||
|
|
15f8e927f0 | ||
|
|
7382723e75 | ||
|
|
3a84ef09f6 | ||
|
|
edd1819c86 | ||
|
|
9ab2c6b01a | ||
|
|
0ef0de45ff |
@@ -61,3 +61,16 @@ make build / watch / lint # Makefile wrappers for above
|
||||
- **No tests**: `npm test` is a stub (exits 1). No test framework installed.
|
||||
- **No CI**: `.github/` is gitignored; no workflows configured.
|
||||
- **HotReload**: `system.json` flags hotReload for css, scss, hbs, json extensions.
|
||||
|
||||
|
||||
## Constraints and remarks
|
||||
|
||||
Foundryv14 only : foundryvtt.com/api/
|
||||
|
||||
## Tests
|
||||
|
||||
With chrome-devtools on port 9222
|
||||
|
||||
World is called "Vermine", thru https://localhost:31000, with Gamemaster user logged in (no password needed)
|
||||
|
||||
Always try to re-use existing chrome-devtools session.
|
||||
|
||||
+31
-6
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"NAME": "Name",
|
||||
"NONE": "None",
|
||||
"ORIGIN": "Origin",
|
||||
"PROFILE": "Profile",
|
||||
"THEME": "Concept",
|
||||
"TOTEM": "Totem",
|
||||
"SETTINGS": {
|
||||
"world": {
|
||||
"game_mode": {
|
||||
@@ -197,13 +203,19 @@
|
||||
"effort": "Effort",
|
||||
"equip": "Equip",
|
||||
"unequip": "Unequip",
|
||||
"group": "Group",
|
||||
"abilities": "Characteristics",
|
||||
"ability": "Characteristic",
|
||||
"skills": "Skills",
|
||||
"skill": "Skill",
|
||||
"skills_title": "Skills",
|
||||
"skill_title": "Skill",
|
||||
"skill_mastery": "Mastery",
|
||||
"specificLevel": "Specific level",
|
||||
"competence": "Skill",
|
||||
"modes": "Game modes",
|
||||
"traits_selector": "Trait selector",
|
||||
"bonus": "Bonus",
|
||||
"bonus_supplementary": "Supplementary bonus",
|
||||
"penalty": "Penalty",
|
||||
"reroll": "Reroll",
|
||||
"equipment": "Equipment",
|
||||
@@ -212,6 +224,9 @@
|
||||
"techniques": "Techniques",
|
||||
"difficulty": "Difficulty",
|
||||
"help": "Help",
|
||||
"add": "Add",
|
||||
"delete": "Delete",
|
||||
"learning": "Learning",
|
||||
"tooling": "Equipment",
|
||||
"quantity": "Quantity",
|
||||
"qty": "Qty",
|
||||
@@ -333,6 +348,9 @@
|
||||
"CharacterNamePlaceholder": "Character Name",
|
||||
"ProfilePlaceholder": "Profile",
|
||||
"none": "None",
|
||||
"all_characters": "All characters",
|
||||
"no_group": "No group",
|
||||
"include_in_phase": "Include in this phase",
|
||||
"bonuses": "Bonuses",
|
||||
"choose_ability": "Choose Ability",
|
||||
"choose_skill": "Choose Skill",
|
||||
@@ -359,7 +377,6 @@
|
||||
"pool_specialty": "Specialty",
|
||||
"pool_help": "Help",
|
||||
"pool_tooling": "Equipment",
|
||||
"pool_group": "Group",
|
||||
"pool_selfControl": "Self-Control",
|
||||
"pool_pools": "Pools",
|
||||
"pool_total": "Total",
|
||||
@@ -469,7 +486,7 @@
|
||||
"mode_survie": "Survival",
|
||||
"mode_cauchemar": "Nightmare",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
|
||||
"evolution_buy_hint": "The purchase consumes a number of Evolution dice equal to the chosen level (the remainder is kept). Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
|
||||
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Invalid level (1 to 4).",
|
||||
@@ -514,6 +531,8 @@
|
||||
"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.",
|
||||
"socle_competences_hint": "Socle de compétences atout: +{level} free Experience die (skill already possessed).",
|
||||
"socle_competences_message": "Socle de compétences: +{level} free Experience die. Reminder: a Group companion who learns or develops with him a skill he possesses also gains 1 Experience die.",
|
||||
"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).",
|
||||
@@ -593,8 +612,9 @@
|
||||
"prohibits": "Prohibits",
|
||||
"prohibits_placeholder": "eg: Fire, Water",
|
||||
"rarity_0": "Common",
|
||||
"rarity_1": "Rare",
|
||||
"rarity_2": "Very Rare",
|
||||
"rarity_1": "Uncommon",
|
||||
"rarity_2": "Rare",
|
||||
"rarity_3": "Very rare",
|
||||
"reserves": "Reserves",
|
||||
"ritual": "Ritual",
|
||||
"sexes": {
|
||||
@@ -1050,9 +1070,14 @@
|
||||
"female": "Feminine"
|
||||
},
|
||||
"COMBAT": {
|
||||
"Begin": "Start Combat",
|
||||
"Delete": "Delete",
|
||||
"Encounter": "Encounter",
|
||||
"End": "End Combat",
|
||||
"None": "None",
|
||||
"Round": "Round"
|
||||
"NotStarted": "Not Started",
|
||||
"Round": "Round",
|
||||
"TurnEnd": "End Turn"
|
||||
},
|
||||
"SIZE_LEVELS": {
|
||||
"tiny": "Insect",
|
||||
|
||||
+11
-3
@@ -248,7 +248,6 @@
|
||||
"pool_specialty": "Spécialité",
|
||||
"pool_help": "Entraide",
|
||||
"pool_tooling": "Équipement",
|
||||
"pool_group": "Groupe",
|
||||
"pool_selfControl": "Sang-Froid",
|
||||
"pool_pools": "Réserves",
|
||||
"pool_total": "Total",
|
||||
@@ -274,7 +273,6 @@
|
||||
"ConfrontationHint": "Résolvez la confrontation en sélectionnant des dés",
|
||||
"PurposeTrait": "Trait de but",
|
||||
"SpleenTrait": "Trait de rate",
|
||||
"group": "Groupe",
|
||||
"abilities": "Caractéristiques",
|
||||
"ability": "Caractéristique",
|
||||
"skills": "Compétences",
|
||||
@@ -283,6 +281,7 @@
|
||||
"skill_title": "Compétence",
|
||||
"skill_mastery": "Maîtrise",
|
||||
"bonus": "Bonus",
|
||||
"bonus_supplementary": "Bonus supplémentaire",
|
||||
"penalty": "Malus",
|
||||
"reroll": "Relance",
|
||||
"equipment": "Equipement",
|
||||
@@ -293,6 +292,9 @@
|
||||
"techniques": "Techniques",
|
||||
"difficulty": "Difficulté",
|
||||
"help": "Entraide",
|
||||
"add": "Ajouter",
|
||||
"delete": "Supprimer",
|
||||
"learning": "Apprentissage",
|
||||
"tooling": "Equipement",
|
||||
"quantity": "Quantité",
|
||||
"qty": "Qté",
|
||||
@@ -365,6 +367,9 @@
|
||||
"choose_ability": "Choisissez une caractéristique",
|
||||
"choose_skill": "Choisissez une compétence",
|
||||
"none": "Aucun",
|
||||
"all_characters": "Tous les personnages",
|
||||
"no_group": "Sans groupe",
|
||||
"include_in_phase": "Inclure dans cette phase",
|
||||
"total": "Total",
|
||||
"bonuses": "Bonus",
|
||||
"handicap": "Handicap",
|
||||
@@ -452,7 +457,7 @@
|
||||
"mode_survie": "Survie",
|
||||
"mode_cauchemar": "Cauchemar",
|
||||
"mode_apocalypse": "Apocalypse",
|
||||
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
|
||||
"evolution_buy_hint": "L'achat consomme un nombre de Dés d'Évolution égal au niveau choisi (le reste est conservé). Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
|
||||
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
|
||||
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
|
||||
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
|
||||
@@ -497,6 +502,8 @@
|
||||
"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.",
|
||||
"socle_competences_hint": "Atout Socle de compétences : +{level}D d'Expérience gratuit (compétence déjà possédée).",
|
||||
"socle_competences_message": "Socle de compétences : +{level}D d'Expérience gratuit. Rappel : un camarade du Groupe qui apprend ou développe avec lui une Compétence qu'il possède gagne aussi 1D d'Expérience.",
|
||||
"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).",
|
||||
@@ -1068,6 +1075,7 @@
|
||||
},
|
||||
"COMBAT": {
|
||||
"Begin": "Commencer",
|
||||
"Delete": "Supprimer",
|
||||
"Encounter": "Rencontre",
|
||||
"End": "Terminer",
|
||||
"None": "Aucun",
|
||||
|
||||
@@ -627,6 +627,15 @@ ol.chat-log {
|
||||
&.visible { max-height: 15rem; }
|
||||
}
|
||||
|
||||
.reroll-from-effort {
|
||||
h4.flexcol { gap: 2px; }
|
||||
.effort-pool {
|
||||
font-size: @font-size-12;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dice results ────────────────────────────────────────────────
|
||||
ul.roll-results {
|
||||
list-style: none;
|
||||
|
||||
@@ -124,13 +124,13 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
|
||||
}
|
||||
|
||||
static async #onReliabilityTest() {
|
||||
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const { default: ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const dialog = await ReliabilityDialog.create({ item: this.document, mode: "test" })
|
||||
await dialog.render(true)
|
||||
}
|
||||
|
||||
static async #onReliabilityRepair() {
|
||||
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const { default: ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||
const dialog = await ReliabilityDialog.create({ item: this.document, mode: "repair" })
|
||||
await dialog.render(true)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
/**
|
||||
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
|
||||
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
|
||||
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
|
||||
* les Dés d'Évolution accumulés.
|
||||
* restrictions sur les Mutations ; en cas de succès, crée l'item et consomme
|
||||
* un nombre de Dés d'Évolution égal au niveau choisi.
|
||||
*/
|
||||
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
@@ -61,7 +61,7 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
|
||||
};
|
||||
const levels = [];
|
||||
for (let value = 1; value <= 4; value++) {
|
||||
levels.push({ value, cost: value });
|
||||
levels.push({ value });
|
||||
}
|
||||
return {
|
||||
evolutionDice: actor.system?.experience?.evolution || 0,
|
||||
@@ -73,6 +73,18 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
/** Met à jour le total des niveaux « après achat » selon le niveau sélectionné. */
|
||||
#updateUI() {
|
||||
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
|
||||
const el = this.element.querySelector("#evolution-total-value");
|
||||
if (el) el.textContent = `${VermineExperience.evolutionTotalLevels(this.#actor) + cost}D`;
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
@@ -102,12 +114,12 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
|
||||
mutation
|
||||
}
|
||||
}]);
|
||||
// N'efface les Dés d'Évolution que si l'item a bien été créé.
|
||||
// Ne consomme que le coût du niveau choisi (Règles p. 116-117 : coût = niveau).
|
||||
if (!created || created.length === 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
|
||||
return;
|
||||
}
|
||||
await actor.update({ "system.experience.evolution": 0 });
|
||||
await actor.update({ "system.experience.evolution": Math.max(0, (actor.system?.experience?.evolution || 0) - cost) });
|
||||
|
||||
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
|
||||
this.close();
|
||||
|
||||
@@ -119,9 +119,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
const creatureLocked = !isCreature && creatureTargets.length && creatureTargets.length === targets.length;
|
||||
if (creatureLocked) {
|
||||
const ref = creatureTargets[0];
|
||||
const creature = ref.id ? game.actors.get(ref.id) : null;
|
||||
const actor = creature ?? (ref.uuid ? await fromUuid(ref.uuid) : null);
|
||||
const d = parseInt(actor?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
// uuid d'abord : un token non lié ne doit pas résoudre vers l'acteur
|
||||
// monde dont il est issu (même principe que VermineExchange).
|
||||
const creature = await VermineExchange.resolveDefender({ id: ref.id, uuid: ref.uuid });
|
||||
const d = parseInt(creature?.system?.combatStatus?.difficulty, 10) || 7;
|
||||
lockedDifficulty = d;
|
||||
difficultyOptions = [{ difficulty: d, label: "", locked: true }];
|
||||
defaultDifficulty = d;
|
||||
@@ -153,7 +154,8 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
lockedDifficulty,
|
||||
defaultDifficulty,
|
||||
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||
// Specialties are only usable when they match the attack skill.
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty" && i.system?.skill === this.skillKey),
|
||||
availableItems: actor.items.filter(i => i.type === "item"),
|
||||
targets,
|
||||
lourdMalus: VermineTraits.lourdMalus(actor),
|
||||
@@ -266,8 +268,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
}
|
||||
|
||||
getMaxEffort() {
|
||||
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
|
||||
return 0;
|
||||
if (this.actor.type !== "character") return 0;
|
||||
const ability = this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
|
||||
const effort = this.actor.system?.attributes?.effort?.value || 0;
|
||||
return Math.min(ability, effort);
|
||||
}
|
||||
|
||||
getPoolBreakdown() {
|
||||
@@ -330,6 +334,14 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
if (handSel && handEl) {
|
||||
handEl.textContent = handSel.options[handSel.selectedIndex]?.text ?? "";
|
||||
}
|
||||
|
||||
const specChecked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
||||
const specEl = this.#el.querySelector(".current-specialty");
|
||||
if (specEl && specChecked) {
|
||||
specEl.textContent = specChecked.value === "aucune"
|
||||
? game.i18n.localize("VERMINE.none")
|
||||
: specChecked.value;
|
||||
}
|
||||
}
|
||||
|
||||
#onInputChange() {
|
||||
|
||||
@@ -48,13 +48,18 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
if (!memberOf.has(id)) memberOf.set(id, g.id);
|
||||
}
|
||||
}
|
||||
const groupById = new Map(groups.map(g => [g.id, g]));
|
||||
return {
|
||||
characters: characters.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId: memberOf.get(a.id) || ""
|
||||
})),
|
||||
characters: characters.map(a => {
|
||||
const groupId = memberOf.get(a.id) || "";
|
||||
return {
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId,
|
||||
groupName: groupId ? groupById.get(groupId)?.name : ""
|
||||
};
|
||||
}),
|
||||
groups: groups.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
@@ -72,6 +77,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.element.querySelectorAll("[name^='include-']").forEach(cb =>
|
||||
cb.addEventListener("click", event => event.stopPropagation())
|
||||
);
|
||||
this.#updateUI();
|
||||
this.#updateGroupFilter();
|
||||
}
|
||||
@@ -84,11 +92,16 @@ 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). */
|
||||
/** Filtre les personnages selon le groupe sélectionné (vide = tous, "none" = sans groupe). */
|
||||
#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;
|
||||
const rowGroupId = row.dataset.groupId || "";
|
||||
const show = groupId === ""
|
||||
? true
|
||||
: groupId === "none"
|
||||
? rowGroupId === ""
|
||||
: rowGroupId === groupId;
|
||||
row.hidden = !show;
|
||||
}
|
||||
}
|
||||
@@ -102,8 +115,8 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
+ (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 groupId = this.element.querySelector("#group-select")?.value || "";
|
||||
if (groupId && groupId !== "none") {
|
||||
const group = game.actors.get(groupId);
|
||||
if (group && collective > 0) {
|
||||
const current = group.system?.experience?.dice || 0;
|
||||
@@ -121,8 +134,11 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
|
||||
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;
|
||||
// Filtre par groupe sélectionné (vide = tous, "none" = sans groupe)
|
||||
const rowGroupId = row.dataset.groupId || "";
|
||||
if (groupId === "none" ? rowGroupId !== "" : groupId !== "" && rowGroupId !== groupId) continue;
|
||||
// Case « Inclure » décochée → personnage exclu de cette phase
|
||||
if (!row.querySelector(`[name="include-${row.dataset.characterId}"]`)?.checked) continue;
|
||||
const id = row.dataset.characterId;
|
||||
const actor = game.actors.get(id);
|
||||
if (!actor) continue;
|
||||
@@ -164,7 +180,7 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
}
|
||||
|
||||
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
|
||||
if (groupId) {
|
||||
if (groupId && groupId !== "none") {
|
||||
const group = game.actors.get(groupId);
|
||||
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
|
||||
}
|
||||
|
||||
@@ -145,6 +145,20 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
return { total: items.length, byLevel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bonus de l'atout "Socle de compétences" (règles p. 112).
|
||||
* Rend le niveau (1-3) de l'atout possédé par le personnage, ou 0.
|
||||
* @returns {number}
|
||||
*/
|
||||
#socleCompetencesBonus() {
|
||||
const item = (this.#actor.itemTypes?.ability || []).find(i =>
|
||||
i.system?.type === "character"
|
||||
&& (i.name || "").trim().toLowerCase() === "socle de compétences".toLowerCase()
|
||||
);
|
||||
if (!item) return 0;
|
||||
return Math.min(3, Math.max(1, parseInt(item.system?.level?.value, 10) || 1));
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
@@ -201,13 +215,16 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
|
||||
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
|
||||
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
||||
// Atout "Socle de compétences" : 1D gratuit si la Compétence est déjà possédée.
|
||||
const socleBonus = current >= 1 ? this.#socleCompetencesBonus() : 0;
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel,
|
||||
difficulty: domain ? Math.max(3, diff - 2) : diff,
|
||||
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
|
||||
poolModifier: 0,
|
||||
poolModifier: socleBonus,
|
||||
socleBonus,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
||||
};
|
||||
@@ -363,6 +380,15 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
set("#diff-value", c.difficulty);
|
||||
set("#handicap-value", c.handicap);
|
||||
set("#required-value", 1 + c.handicap);
|
||||
const socleHint = this.#el.querySelector("#socle-bonus-hint");
|
||||
if (socleHint) {
|
||||
if (type === "skill" && c.socleBonus > 0) {
|
||||
socleHint.style.display = "";
|
||||
socleHint.textContent = game.i18n.format("VERMINE.socle_competences_hint", { level: c.socleBonus });
|
||||
} else {
|
||||
socleHint.style.display = "none";
|
||||
}
|
||||
}
|
||||
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
|
||||
let poolText = `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`;
|
||||
if (c.adaptedDie) poolText += " — " + game.i18n.localize("VERMINE.adapted_die_x2");
|
||||
@@ -496,6 +522,14 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
if ((roll._total ?? 0) >= required) {
|
||||
await this.#applySuccess(c, specialtyName, roll._total ?? 0);
|
||||
}
|
||||
// Atout "Socle de compétences" : rappeler le bonus conféré aux camarades
|
||||
// du Groupe qui apprennent une Compétence possédée par le personnage.
|
||||
if (c.socleBonus > 0) {
|
||||
await ChatMessage.create({
|
||||
content: game.i18n.format("VERMINE.socle_competences_message", { level: c.socleBonus }),
|
||||
speaker: ChatMessage.getSpeaker({ actor })
|
||||
});
|
||||
}
|
||||
// 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();
|
||||
|
||||
@@ -288,7 +288,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
|
||||
getMaxEffort() {
|
||||
const sel = this.#getAbility();
|
||||
return parseInt(sel?.value, 10) || 0;
|
||||
const ability = parseInt(sel?.value, 10) || 0;
|
||||
const effort = this.#actor?.system?.attributes?.effort?.value || 0;
|
||||
return Math.min(ability, effort);
|
||||
}
|
||||
|
||||
getTotems() {
|
||||
|
||||
+52
-23
@@ -258,17 +258,19 @@ export class VermineExchange {
|
||||
* Résout l'échange pour une cible donnée (défense la plus récente liée
|
||||
* à cette cible, ou abstention si aucune défense).
|
||||
* @param {ChatMessage} attackMessage
|
||||
* @param {string|null} targetId id d'acteur de la cible (défaut : première cible)
|
||||
* @param {Object|string|null} targetRef cible de l'attaque (ou son uuid),
|
||||
* défaut : première cible — le matching se fait par uuid d'abord car des
|
||||
* PNJ mineurs (tokens non liés) issus de la même fiche partagent le même id
|
||||
* @returns {Promise<Object|null>} résultat de la résolution
|
||||
*/
|
||||
static async resolve(attackMessage, targetId = null) {
|
||||
static async resolve(attackMessage, targetRef = null) {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return null
|
||||
|
||||
const target = (targetId && exchange.targets?.find(t => t.id === targetId)) || exchange.targets?.[0] || null
|
||||
const target = this.#matchTarget(exchange.targets || [], targetRef)
|
||||
if (!target) return null
|
||||
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target.id)
|
||||
const defs = this.#defenseMessagesFor(exchange.id, target)
|
||||
const defenderMessage = defs[defs.length - 1]
|
||||
const defense = defenderMessage ? defenderMessage.getFlag('world', FLAG_DEFENSE) : null
|
||||
|
||||
@@ -281,7 +283,7 @@ export class VermineExchange {
|
||||
const defenderRef = defense
|
||||
? { id: defense.actorId ?? null, uuid: defense.actorUuid ?? null, name: defense.actorName ?? '' }
|
||||
: target
|
||||
const defender = await this.#resolveDefender(defenderRef)
|
||||
const defender = await this.resolveDefender(defenderRef)
|
||||
|
||||
const damage = (exchange.baseDamage || 0) + attackerSuccesses
|
||||
const parryLevel = defense?.type === 'parade' ? (defense.parryLevel ?? 0) : null
|
||||
@@ -384,7 +386,7 @@ export class VermineExchange {
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
const type = btn.dataset.type
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
const actor = await this.resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
@@ -474,7 +476,7 @@ export class VermineExchange {
|
||||
|
||||
const targets = exchange.targets?.length ? exchange.targets : [null]
|
||||
for (const target of targets) {
|
||||
const result = await this.resolve(attackMessage, target?.id ?? null)
|
||||
const result = await this.resolve(attackMessage, target)
|
||||
if (!result) continue
|
||||
const block = document.createElement('div')
|
||||
block.innerHTML = this.renderResolution(result)
|
||||
@@ -491,7 +493,7 @@ export class VermineExchange {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
const actor = await this.resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
@@ -541,9 +543,9 @@ export class VermineExchange {
|
||||
}
|
||||
let applied = 0
|
||||
for (const target of exchange.targets) {
|
||||
const r = await this.resolve(attackMessage, target?.id ?? null)
|
||||
const r = await this.resolve(attackMessage, target)
|
||||
if (!r?.hit || !r.woundCategory) continue
|
||||
const defender = await this.#resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
const defender = await this.resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
if (!defender || !this.canApplyWound(defender)) continue
|
||||
const result = await this.applyWound(defender, r.woundCategory)
|
||||
if (result && result !== false) applied += 1
|
||||
@@ -559,7 +561,7 @@ export class VermineExchange {
|
||||
static async #onApply(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
const defender = await this.#resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const defender = await this.resolveDefender({ id: btn.dataset.defenderId, uuid: btn.dataset.defenderUuid })
|
||||
const wound = btn.dataset.wound
|
||||
if (!defender || !wound) return
|
||||
if (!this.canApplyWound(defender)) {
|
||||
@@ -588,31 +590,58 @@ export class VermineExchange {
|
||||
// ── Helpers internes ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Résout un acteur depuis son id d'acteur monde (acteurs liés) ou son
|
||||
* uuid (tokens non liés via fromUuid).
|
||||
* Résout l'acteur d'une cible depuis sa référence (id d'acteur monde ou
|
||||
* uuid). Le uuid est privilégié : pour un token non lié, `token.actor.id`
|
||||
* expose encore l'id de l'acteur monde dont le token est issu, et résoudre
|
||||
* par id blesserait alors tous les tokens liés à cette fiche (PNJ mineurs).
|
||||
* Le uuid d'un acteur synthétique ("Scene.x.Token.y.Actor.z") n'est pas
|
||||
* résolvable de façon fiable par fromUuid (document non indexé quand la
|
||||
* résolution est asynchrone) : on extrait donc directement le token.
|
||||
* @param {{id: string|null, uuid: string|null}} ref
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveDefender({ id = null, uuid = null }) {
|
||||
static async resolveDefender({ id = null, uuid = null }) {
|
||||
if (uuid) {
|
||||
const parts = uuid.split('.')
|
||||
if (parts[0] === 'Scene' && parts[2] === 'Token') {
|
||||
const token = game.scenes.get(parts[1])?.tokens.get(parts[3])
|
||||
return token?.actor ?? null
|
||||
}
|
||||
if (parts[0] === 'Actor') {
|
||||
return game.actors.get(parts[1]) ?? null
|
||||
}
|
||||
}
|
||||
if (id) {
|
||||
const actor = game.actors.get(id)
|
||||
if (actor) return actor
|
||||
}
|
||||
if (uuid) {
|
||||
const doc = await fromUuid(uuid)
|
||||
if (!doc) return null
|
||||
if (doc.documentType === 'Actor') return doc
|
||||
if (doc.documentType === 'Token') return doc.actor ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, targetId = null) {
|
||||
/**
|
||||
* Retrouve la cible d'un échange par uuid d'abord, puis par id. Le uuid est
|
||||
* fiable pour distinguer des PNJ mineurs (tokens non liés) issus de la même
|
||||
* fiche, qui partagent le même id d'acteur monde.
|
||||
* @param {Object[]} targets cibles du flag d'échange
|
||||
* @param {Object|string|null} ref référence de la cible (ou son uuid)
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
static #matchTarget(targets, ref) {
|
||||
if (!ref) return targets[0] ?? null
|
||||
const uuid = typeof ref === 'string' ? ref : (ref.uuid ?? null)
|
||||
const id = typeof ref === 'string' ? null : (ref.id ?? null)
|
||||
if (uuid) return targets.find(t => t.uuid === uuid) ?? null
|
||||
if (id) return targets.find(t => t.id === id) ?? null
|
||||
return targets[0] ?? null
|
||||
}
|
||||
|
||||
static #defenseMessagesFor(exchangeId, target = null) {
|
||||
return game.messages.contents.filter(m => {
|
||||
const f = m.getFlag('world', FLAG_DEFENSE)
|
||||
if (!f || f.exchangeId !== exchangeId) return false
|
||||
if (targetId) return f.actorId === targetId
|
||||
return true
|
||||
if (!target) return true
|
||||
if (target.uuid && f.actorUuid === target.uuid) return true
|
||||
return f.actorId === target.id
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ export const registerHooks = function () {
|
||||
}
|
||||
})
|
||||
Hooks.once("ready", async () => {
|
||||
// System initialized
|
||||
//await registerTours();
|
||||
|
||||
// Relays player rerolls to the GM client (Foundry v14 ChatMessage
|
||||
// updates are GM-only).
|
||||
VermineUtils.registerRerollSocket();
|
||||
});
|
||||
|
||||
// changement de la pause
|
||||
|
||||
+100
-17
@@ -204,9 +204,10 @@ export class VermineUtils {
|
||||
* @returns {Promise<boolean>} Whether the reroll was successful
|
||||
*/
|
||||
static async onReroll(message, target) {
|
||||
// Verify user permissions
|
||||
const msgUserId = message.user?.id ?? message.user;
|
||||
if (msgUserId !== game.user?.id && !game.user?.isGM) {
|
||||
// Verify user permissions. Foundry v14 renamed the ChatMessage field
|
||||
// `user` to `author`, so `message.user` is undefined on this core.
|
||||
const msgAuthorId = message.author?.id ?? message.user?.id ?? message.user;
|
||||
if (msgAuthorId !== game.user?.id && !game.user?.isGM) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_cannot_reroll'));
|
||||
return false;
|
||||
}
|
||||
@@ -231,9 +232,6 @@ export class VermineUtils {
|
||||
|
||||
target.classList.add('rerolled');
|
||||
|
||||
// Set reroll flag
|
||||
await message.setFlag("world", "reroll", true);
|
||||
|
||||
// Get difficulty and dice type
|
||||
const ulElement = target.closest('ul');
|
||||
const difficulty = ulElement?.dataset.difficulty ?? 7;
|
||||
@@ -331,14 +329,55 @@ export class VermineUtils {
|
||||
+ mk('adapted', stats.adapted.count, stats.adapted.success);
|
||||
}
|
||||
|
||||
await message.update({
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
// Persist the reroll. The GM writes the message directly ; players
|
||||
// cannot update ChatMessages on Foundry v14 (permission "OWNER"), so
|
||||
// their reroll is relayed through the system socket and applied by the
|
||||
// GM client (see registerRerollSocket).
|
||||
if (game.user?.isGM) {
|
||||
await message.setFlag("world", "reroll", true);
|
||||
await message.update({
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
} else {
|
||||
game.socket?.emit(`system.${game.system.id}`, {
|
||||
type: "reroll",
|
||||
messageId: message.id,
|
||||
requestId: foundry.utils.randomID(8),
|
||||
content: messageContent.outerHTML
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the system socket listener which applies rerolls requested by
|
||||
* players. Foundry v14 only allows GMs to update ChatMessages, so player
|
||||
* rerolls are relayed to the GM client through `system.<id>` (see onReroll).
|
||||
* Call once after the world is ready; re-registration replaces the handler.
|
||||
*/
|
||||
static registerRerollSocket() {
|
||||
const eventName = `system.${game.system.id}`;
|
||||
const handler = async (payload, userId) => {
|
||||
if (!game.user?.isGM) return;
|
||||
if (payload?.type !== "reroll") return;
|
||||
const message = game.messages.get(payload.messageId);
|
||||
if (!message) return;
|
||||
// Security: only the message author may request the reroll update.
|
||||
if (message.author?.id !== userId) return;
|
||||
await message.setFlag("world", "reroll", true);
|
||||
await message.update({ content: payload.content });
|
||||
};
|
||||
if (VermineUtils.#rerollSocketHandler) {
|
||||
game.socket?.off(eventName, VermineUtils.#rerollSocketHandler);
|
||||
}
|
||||
VermineUtils.#rerollSocketHandler = handler;
|
||||
game.socket?.on(eventName, handler);
|
||||
}
|
||||
|
||||
static #rerollSocketHandler;
|
||||
|
||||
/**
|
||||
* Sets up event listeners for chat messages.
|
||||
* @param {HTMLElement} html - The HTML element containing chat events.
|
||||
@@ -376,34 +415,78 @@ export class VermineUtils {
|
||||
});
|
||||
}
|
||||
|
||||
// Add click event for granting rerolls
|
||||
// Add click event for spending Effort points as rerolls (règles p.35) :
|
||||
// 1 point d'Effort = 1 relance, ajouté aux relances de Compétence restantes.
|
||||
html.querySelectorAll("button.grant-reroll").forEach(el => {
|
||||
el.addEventListener('click', async (ev) => {
|
||||
const btn = ev.currentTarget;
|
||||
const rollMessage = btn.closest(".vermine-roll-message");
|
||||
const grantedRerollElement = html.querySelector('#granted-reroll');
|
||||
const allowedRerollElement = html.querySelector("#allowed_reroll");
|
||||
const requested = parseInt(grantedRerollElement?.innerText, 10) || 0;
|
||||
|
||||
if (grantedRerollElement && allowedRerollElement) {
|
||||
allowedRerollElement.innerText = grantedRerollElement.innerText;
|
||||
// Déduit les points d'Effort de l'acteur (max : ce qu'il lui reste).
|
||||
const actor = await VermineUtils.#resolveMessageActor(rollMessage);
|
||||
const effort = actor?.system?.attributes?.effort;
|
||||
let spent = 0;
|
||||
if (effort && requested > 0) {
|
||||
spent = Math.min(requested, parseInt(effort.value, 10) || 0);
|
||||
if (spent > 0) {
|
||||
const remaining = (parseInt(effort.value, 10) || 0) - spent;
|
||||
await actor.update({ 'system.attributes.effort.value': remaining });
|
||||
const poolEl = rollMessage?.querySelector('.effort-pool');
|
||||
if (poolEl) poolEl.innerText = `${remaining}/${effort.max || 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Les relances d'Effort s'ajoutent aux relances de Compétence restantes.
|
||||
if (allowedRerollElement) {
|
||||
const current = parseInt(allowedRerollElement.innerText, 10) || 0;
|
||||
allowedRerollElement.innerText = current + spent;
|
||||
}
|
||||
const effortSection = btn.closest('.reroll-from-effort');
|
||||
if (effortSection) effortSection.style.display = "none";
|
||||
|
||||
const mesEl = btn.closest('[data-message-id]');
|
||||
const messageId = mesEl?.dataset?.messageId;
|
||||
|
||||
if (messageId) {
|
||||
btn.closest('.reroll-from-effort').style.display = "none";
|
||||
|
||||
const rollMessage = btn.closest(".vermine-roll-message");
|
||||
if (rollMessage) {
|
||||
const content = rollMessage.outerHTML;
|
||||
const content = rollMessage.outerHTML;
|
||||
// GM applies the update directly ; players relay through the
|
||||
// system socket (Foundry v14 ChatMessage updates are GM-only).
|
||||
if (game.user?.isGM) {
|
||||
const message = await game.messages.get(messageId);
|
||||
await message.update({ content: content });
|
||||
} else {
|
||||
game.socket?.emit(`system.${game.system.id}`, {
|
||||
type: "reroll",
|
||||
messageId: messageId,
|
||||
requestId: foundry.utils.randomID(8),
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout l'acteur à l'origine d'un message de jet depuis les attributs
|
||||
* data-actor-id / data-actor-uuid posés sur le message. Le uuid est
|
||||
* privilégié : il distingue les tokens non liés (PNJ mineurs) issus de la
|
||||
* même fiche, qui partagent le même id d'acteur monde.
|
||||
* @param {HTMLElement} rollMessage
|
||||
* @returns {Promise<Actor|null>}
|
||||
*/
|
||||
static async #resolveMessageActor(rollMessage) {
|
||||
if (!rollMessage) return null;
|
||||
const id = rollMessage.dataset.actorId ?? null;
|
||||
const uuid = rollMessage.dataset.actorUuid ?? null;
|
||||
if (!id && !uuid) return null;
|
||||
const { VermineExchange } = await import('./exchange.mjs');
|
||||
return VermineExchange.resolveDefender({ id, uuid });
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays dice rolls in 3D if available.
|
||||
* @param {Roll} roll - The roll to display
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"distance": 1,
|
||||
"units": "m"
|
||||
},
|
||||
"socket": true,
|
||||
"primaryTokenAttribute": "health",
|
||||
"secondaryTokenAttribute": "power",
|
||||
"url": "https://www.uberwald.me/gitea/uberwald/vermine2047",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<label class="label" for="evolution-level">{{ localize 'VERMINE.evolution_level' }}</label>
|
||||
<select class="info-value" data-roll="true" id="evolution-level" name="evolution-level">
|
||||
{{#each levels}}
|
||||
<option value="{{ this.value }}">{{ this.value }}D — {{ this.cost }}D</option>
|
||||
<option value="{{ this.value }}">{{ this.value }}D</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SPECIALTIES (limited to the attack skill) -->
|
||||
{{#if availableSpecialties.length}}
|
||||
<details class="specialties-section">
|
||||
<summary class="flexrow">
|
||||
<span class="label">{{localize 'VERMINE.specialty'}} (+1D)</span>
|
||||
<span class="current-specialty">{{localize 'VERMINE.none'}}</span>
|
||||
</summary>
|
||||
<div class="flexrow specialty-options">
|
||||
<label class="flexrow">
|
||||
<input
|
||||
type="radio"
|
||||
data-roll="true"
|
||||
name="usingSpecialization"
|
||||
id="usingSpecialization"
|
||||
value="aucune"
|
||||
checked
|
||||
/>
|
||||
<span>{{localize 'VERMINE.none'}}</span>
|
||||
</label>
|
||||
{{#each availableSpecialties as |spec ind|}}
|
||||
<label class="flexrow">
|
||||
<input
|
||||
type="radio"
|
||||
data-roll="true"
|
||||
name="usingSpecialization"
|
||||
id="usingSpecialization"
|
||||
value="{{spec.name}}"
|
||||
data-spec-skill="{{spec.system.skill}}"
|
||||
/>
|
||||
<span>{{spec.name}}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</details>
|
||||
{{/if}}
|
||||
|
||||
<!-- DIFFICULTY -->
|
||||
<details class="difficulty-section" open>
|
||||
<summary class="flexrow">
|
||||
@@ -165,7 +201,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
|
||||
<label class="label" for="group">{{localize 'VERMINE.bonus_supplementary'}}</label>
|
||||
<input type="number" data-roll="true" class="numeric-entry" style="text-align: center;"
|
||||
name="group" id="group" min="0" max="5" value="0" />
|
||||
<span>D</span>
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
<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>
|
||||
<option value="">{{ localize 'VERMINE.all_characters' }}</option>
|
||||
<option value="none">{{ localize 'VERMINE.no_group' }}</option>
|
||||
{{#each groups}}
|
||||
<option value="{{ this.id }}">{{ this.name }} ({{ this.dice }}D)</option>
|
||||
{{/each}}
|
||||
@@ -49,8 +50,14 @@
|
||||
{{#each characters}}
|
||||
<details class="difficulty-section" data-character-id="{{ this.id }}" data-group-id="{{ this.groupId }}" open>
|
||||
<summary class="flexrow">
|
||||
<label class="flexrow exp-include-label" title="{{ localize 'VERMINE.include_in_phase' }}">
|
||||
<input type="checkbox" data-roll="true" id="include-{{ this.id }}" name="include-{{ this.id }}" checked />
|
||||
</label>
|
||||
<span class="label">{{ this.name }}</span>
|
||||
<span class="current-values">{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D</span>
|
||||
<span class="current-values">
|
||||
{{#if this.groupName}}[{{ this.groupName }}]{{ else }}[{{ localize 'VERMINE.no_group' }}]{{/if}}
|
||||
{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D
|
||||
</span>
|
||||
</summary>
|
||||
<div class="flexrow difficulty-controls">
|
||||
<div class="flexcol">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<p class="learning-hint" id="socle-bonus-hint" style="display:none;"></p>
|
||||
</div>
|
||||
|
||||
<div id="learning-specialty-section" class="learning-targets" style="display:none;">
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
|
||||
<label class="label" for="group">{{localize 'VERMINE.bonus_supplementary'}}</label>
|
||||
<input type="number" data-roll="true" class="numeric-entry" style="text-align: center;"
|
||||
name="group" id="group" min="0" max="5" value="0" />
|
||||
<span>D</span>
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
|
||||
<!-- GROUP -->
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group">{{localize 'VERMINE.group'}}</label>
|
||||
<label class="label" for="group">{{localize 'VERMINE.bonus_supplementary'}}</label>
|
||||
<input
|
||||
type="number"
|
||||
data-roll="true"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
{{#if item.system.effects}}
|
||||
<div class="resource flexcol">
|
||||
<span class="resource-label">{{ localize 'UI.effects' }}:</span>
|
||||
<span class="resource-label">{{ localize 'UI.effects.name' }}:</span>
|
||||
<ul class="unstyled">
|
||||
{{#each item.system.effects as |effect|}}
|
||||
<li>{{effect}}</li>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{{#if item.system.effect}}
|
||||
<div class="resource flexrow">
|
||||
<span class="resource-label">{{ localize 'UI.effects' }}:</span>
|
||||
<span class="resource-label">{{ localize 'UI.effects.name' }}:</span>
|
||||
<span>{{item.system.effect}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="vermine-roll-message" data-free-successes="{{verdict.bonusSuccesses}}">
|
||||
<div class="vermine-roll-message" data-free-successes="{{verdict.bonusSuccesses}}" data-actor-id="{{param.actor.id}}" data-actor-uuid="{{param.actor.uuid}}">
|
||||
{{!-- Verdict --}}
|
||||
<div class="roll-verdict {{#if verdict.success}}success{{else}}failure{{/if}}">
|
||||
{{#if verdict.success}}
|
||||
@@ -191,7 +191,7 @@
|
||||
<span class="pool-item pool-malus">{{localize "VERMINE.pool_psychology"}} -{{param.poolBreakdown.psychology}}D</span>
|
||||
{{/if}}
|
||||
{{#if param.poolBreakdown.group}}
|
||||
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
|
||||
<span class="pool-item">{{localize "VERMINE.bonus_supplementary"}} +{{param.poolBreakdown.group}}D</span>
|
||||
{{/if}}
|
||||
{{#if param.poolBreakdown.pools}}
|
||||
<span class="pool-item">{{localize "VERMINE.pool_pools"}} +{{param.poolBreakdown.pools}}D</span>
|
||||
@@ -226,9 +226,11 @@
|
||||
<div class="reroll-fromroll">
|
||||
<h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4>
|
||||
<div class="reroll flexrow">
|
||||
{{#ifgt param.actor.system.attributes.effort.value 0}}
|
||||
<div class="reroll-from-effort flexrow">
|
||||
<h4 class="flexcol">
|
||||
<span>{{localize "VERMINE.effort"}}</span>
|
||||
<span class="effort-pool">{{param.actor.system.attributes.effort.value}}/{{param.actor.system.attributes.effort.max}}</span>
|
||||
</h4>
|
||||
<input type="range"
|
||||
min="0"
|
||||
@@ -240,6 +242,7 @@
|
||||
<span id="granted-reroll">0</span>
|
||||
</button>
|
||||
</div>
|
||||
{{/ifgt}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user