fix: Dice So Nice import v14 compat, override Foundry text colors for readability
Release Creation / build (release) Failing after 1m28s

- hooks.mjs: replace static dice-so-nice import with dynamic import
  using game.modules.get('dice-so-nice').id (path removed in v14)
- hooks.mjs: fix permission condition (|| -> &&), jQuery -> vanilla JS
- less/base.less: override --color-text-* and --button-text-color
  for both .themed.theme-dark (AppV2) and body.theme-dark (legacy apps)
- target #settings-config buttons + labels + hints for dark grays
This commit is contained in:
2026-07-12 22:05:45 +02:00
parent dcc24b47ec
commit 90de66d668
44 changed files with 516 additions and 4305 deletions
+101 -114
View File
@@ -1,5 +1,4 @@
export class TotemPicker extends Application {
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(linkEl, actor) {
super();
@@ -7,45 +6,39 @@ export class TotemPicker extends Application {
this.actor = actor;
}
/* -------------------------------------------- */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
static get DEFAULT_OPTIONS() {
return {
id: "TOTEM_PICKER",
title: game.i18n.localize("VERMINE.totem_picker"),
template: 'systems/vermine2047/templates/applications/choose-totem.hbs',
popOut: true,
resizable: true,
height: "800",
width: "800"
});
position: { width: 800, height: 800 },
window: { title: game.i18n.localize("VERMINE.totem_picker"), resizable: true }
};
}
getData() {
// Send data to the template
return {
config: CONFIG.VERMINE,
static PARTS = {
main: {
template: 'systems/vermine2047/templates/applications/choose-totem.hbs'
}
};
_prepareContext() {
return { config: CONFIG.VERMINE };
}
activateListeners(html) {
super.activateListeners(html);
html.find('.totem').click(event => {
const totem = $(event.target).parent('a').data('totem');
if (totem != null) {
this.actor.update({ 'system.identity.totem': totem });
}
this.close();
html.querySelectorAll('.totem').forEach(el => {
el.addEventListener('click', event => {
const link = event.target.closest('a');
if (!link?.dataset.totem) return;
this.actor.update({ 'system.identity.totem': link.dataset.totem });
this.close();
});
});
}
/*async _updateObject(event, formData) {
// console.log(formData.exampleInput);
}*/
}
export class ActorPicker extends Application {
export class ActorPicker extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(linkEl, actor) {
super();
@@ -53,152 +46,146 @@ export class ActorPicker extends Application {
this.actor = actor;
}
/* -------------------------------------------- */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
static get DEFAULT_OPTIONS() {
return {
id: "ACTOR_PICKER",
title: game.i18n.localize("VERMINE.actor_picker"),
template: 'systems/vermine2047/templates/applications/choose-actor.hbs',
popOut: true,
resizable: true,
height: "350",
width: "600"
});
position: { width: 600, height: 350 },
window: { title: game.i18n.localize("VERMINE.actor_picker"), resizable: true }
};
}
static PARTS = {
main: {
template: 'systems/vermine2047/templates/applications/choose-actor.hbs'
}
};
getData() {
// Send data to the template
_prepareContext() {
const npcs = game.actors.filter(a => a.type == "npc");
const characters = game.actors.filter(a => a.type == "character");
const all = game.actors.filter(a => a.type == "npc" || a.type == 'character');
const type = $(this.linkEl).data('type');
const type = this.linkEl.dataset.type;
let actorsList = [];
if (type == 'members') {
actorsList = characters;
} else if (type == 'relations') {
actorsList = npcs;//[...npcs, ...characters];
actorsList = npcs;
} else {
actorsList = all;
}
return {
config: CONFIG.VERMINE,
actorsList: actorsList
}
};
}
async activateListeners(html) {
activateListeners(html) {
super.activateListeners(html);
html.find('.actor').click(async (event) => {
const actorId = $(event.target).parent('div').data('actor-id');
const type = $(this.linkEl).data('type');
if (!actorId) return;
let actorsList = [];
html.querySelectorAll('.actor').forEach(el => {
el.addEventListener('click', async event => {
const div = event.target.closest('div');
const actorId = div?.dataset.actorId;
const type = this.linkEl.dataset.type;
if (!actorId) return;
if (type == 'members') {
actorsList = foundry.utils.duplicate(this.actor.system.members || []);
} else if (type == 'encounters') {
actorsList = foundry.utils.duplicate(this.actor.system.encounters || []);
}
if (!Array.isArray(actorsList)) {
actorsList = [];
}
let actorsList = [];
// Add actor if not already present
if (!actorsList.includes(actorId)) {
actorsList.push(actorId);
}
if (type == 'members') {
actorsList = foundry.utils.duplicate(this.actor.system.members || []);
} else if (type == 'encounters') {
actorsList = foundry.utils.duplicate(this.actor.system.encounters || []);
}
if (type == 'members') {
await this.actor.update({ 'system.members': actorsList });
} else if (type == 'encounters') {
await this.actor.update({ 'system.encounters': actorsList });
}
this.close();
if (!Array.isArray(actorsList)) {
actorsList = [];
}
if (!actorsList.includes(actorId)) {
actorsList.push(actorId);
}
if (type == 'members') {
await this.actor.update({ 'system.members': actorsList });
} else if (type == 'encounters') {
await this.actor.update({ 'system.encounters': actorsList });
}
this.close();
});
});
}
}
export class TraitSelector extends Application {
export class TraitSelector extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(targetItem) {
super();
this.targetItem = targetItem;
this.traits = CONFIG.VERMINE.traits
this.traits = CONFIG.VERMINE.traits;
}
/* -------------------------------------------- */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["vermine2047", "trait-selector"],
title: game.i18n.localize("VERMINE.traits_selector"),
template: 'systems/vermine2047/templates/applications/choose-traits.hbs',
popOut: true,
resizable: true,
height: "500",
width: "500"
});
static get DEFAULT_OPTIONS() {
return {
id: "TRAIT_SELECTOR",
position: { width: 500, height: 500 },
window: { title: game.i18n.localize("VERMINE.traits_selector"), resizable: true },
classes: ["vermine2047", "trait-selector"]
};
}
getData() {
static PARTS = {
main: {
template: 'systems/vermine2047/templates/applications/choose-traits.hbs'
}
};
_prepareContext() {
return {
traits: this.traits,
item: this.targetItem
}
};
}
async activateListeners(html) {
super.activateListeners(html);
this.validateTraits(html);
html.find('input').change(ev => {
this.onChangeInput(ev)
})
activateListeners(html) {
super.activateListeners(html);
this._validateTraits(html);
html.querySelectorAll('input').forEach(el => {
el.addEventListener('change', ev => this._onChangeInput(ev));
});
}
async validateTraits(html) {
let checks = html.find("input.trait-selector");
for (let inp of checks) {
async _validateTraits(html) {
const checks = html.querySelectorAll("input.trait-selector");
for (const inp of checks) {
if (this.targetItem.system.traits[inp.dataset.trait]) {
if (inp.type == "checkbox") {
inp.checked = true
inp.checked = true;
}
}
}
await this.render(true)
await this.render(true);
}
async onChangeInput(ev) {
let el = ev.currentTarget;
let traitKey = el.dataset.trait; // Récupère la clé du trait à partir de l'attribut data-trait
let traitValue = parseInt(el.value) || null
let traits = this.targetItem.system.traits || {}; // Récupère les traits actuels, ou un objet vide si aucun trait n'est défini
console.log(traitKey, traitValue, traits)
async _onChangeInput(ev) {
const el = ev.currentTarget;
const traitKey = el.dataset.trait;
const traitValue = parseInt(el.value) || null;
const traits = this.targetItem.system.traits || {};
if (el.classList.contains('trait-selector')) {
if (!traits[traitKey]) {
// Si la case est cochée, ajoute le trait
await this.targetItem.update({ [`system.traits.${traitKey}`]: this.traits[traitKey] });
} else {
// Si la case est décochée, retire le trait
await this.targetItem.update({ [`system.traits.${traitKey}`]: null });
}
}
if (traitValue) {
console.log(el.value)
// Logique pour les valeurs des traits si nécessaire
el.closest("label").querySelector('.trait-selector').checked = true;
} else {
el.closest("label").querySelector('.trait-selector').checked = false;
}
this.render(true)
this.render(true);
}
}
}
+43 -40
View File
@@ -26,46 +26,6 @@ VERMINE.DifficultyLevels = {
3: { "label": "DIFFICULTY_LEVELS.hard", "difficulty": 7 },
4: { "label": "DIFFICULTY_LEVELS.very_hard", "difficulty": 9 },
5: { "label": "DIFFICULTY_LEVELS.impossible", "difficulty": 10 }
},
VERMINE.ThreatLevels = {
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
}
VERMINE.ExperienceLevels = {
1: { "label": "SKILL_LEVELS.beginner", "action": 3, "specialties": 4, "rerolls": 0, "contact": "7" },
2: { "label": "SKILL_LEVELS.proficient", "action": 3, "specialties": 5, "rerolls": 0, "contact": "5 ou 7" },
3: { "label": "SKILL_LEVELS.expert", "action": 4, "specialties": 6, "rerolls": 1, "contact": "5,7 ou 9" },
4: { "label": "SKILL_LEVELS.master", "action": 4, "specialties": 6, "rerolls": 2, "contact": "3,5,7 ou 9" },
}
VERMINE.RoleLevels = {
1: { "label": "ROLE_LEVELS.minor", "reaction": 3, "reaction_bonus": 0, "pools": 0, "gear": 9, "gear_hindrance": 0, "protection": 1 },
2: { "label": "ROLE_LEVELS.secondary", "reaction": 3, "reaction_bonus": 1, "pools": 1, "gear": 9, "gear_hindrance": 1, "protection": 2 },
3: { "label": "ROLE_LEVELS.important", "reaction": 3, "reaction_bonus": 2, "pools": 2, "gear": 9, "gear_hindrance": 2, "protection": 3 },
4: { "label": "ROLE_LEVELS.major", "reaction": 4, "reaction_bonus": 2, "pools": 4, "gear": 10, "gear_hindrance": 2, "protection": 3 },
}
VERMINE.PatternLevels = {
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
}
VERMINE.SizeLevels = {
1: { "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
3: { "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
}
VERMINE.PackLevels = {
1: { "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
}
/**
@@ -251,6 +211,49 @@ VERMINE.skillCategories = {
}
}
/**
* Skills mapping: each skill belongs to a category.
* Used by templates to render skill-select dropdowns.
*/
VERMINE.skills = {
// man
arts: { category: "man" },
civilization: { category: "man" },
psychology: { category: "man" },
rumors: { category: "man" },
healing: { category: "man" },
// animal
animalism: { category: "animal" },
dissection: { category: "animal" },
wildlife: { category: "animal" },
repulsion: { category: "animal" },
tracks: { category: "animal" },
// tool
crafting: { category: "tool" },
diy: { category: "tool" },
mecanical: { category: "tool" },
piloting: { category: "tool" },
technology: { category: "tool" },
// weapon
firearms: { category: "weapon" },
archery: { category: "weapon" },
armory: { category: "weapon" },
throwing: { category: "weapon" },
melee: { category: "weapon" },
// survival
alertness: { category: "survival" },
atletics: { category: "survival" },
food: { category: "survival" },
stealth: { category: "survival" },
close: { category: "survival" },
// world
environment: { category: "world" },
flora: { category: "world" },
road: { category: "world" },
toxics: { category: "world" },
ruins: { category: "world" }
}
VERMINE.sexes = { "male": "SEXES.male", "female": "SEXES.female" };
VERMINE.totems = {
-37
View File
@@ -1,37 +0,0 @@
import { VermineUtils } from "./roll.mjs";
export class CombatResultDialog extends Dialog {
constructor(dialogData, options) {
/*let options = { classes: ["combat", "result"], ...options };
let conf = {
title: "Résultat de la confrontation",
content: dialogData.content
};
super(conf, options);
this.dialogData = dialogData;*/
}
/* -------------------------------------------- */
activateListeners(html) {
/*super.activateListeners(html);
this.html = html;
this.setEphemere(this.dialogData.signe.system.ephemere);
html.find(".signe-aleatoire").click(event => this.setSigneAleatoire());
html.find("[name='signe.system.ephemere']").change((event) => this.setEphemere(event.currentTarget.checked));
html.find(".signe-xp-sort").change((event) => this.onValeurXpSort(event));
html.find("input.select-actor").change((event) => this.onSelectActor(event));
html.find("input.select-tmr").change((event) => this.onSelectTmr(event));*/
}
async onSelectActor(event) {
/*const actorId = this.html.find(event.currentTarget)?.data("actor-id");
const actor = this.dialogData.actors.find(it => it.id == actorId);
if (actor) {
actor.selected = event.currentTarget.checked;
}*/
}
}
+1 -1
View File
@@ -139,7 +139,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getHandicapSelect() {
const sel = this.#getHandicap();
return parseInt(sel?.value, 10) || 1;
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
}
getSkillCategory() {
+8 -8
View File
@@ -43,19 +43,19 @@ export async function initUserDice(dice3d, user) {
export function darkenColor(color, percent) {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = ((num >> 8) & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
const R = (num >> 16) - amt;
const G = ((num >> 8) & 0x00FF) - amt;
const B = (num & 0x0000FF) - amt;
return '#' + (0x1000000 + (R < 0 ? 0 : R > 255 ? 255 : R) * 0x10000 + (G < 0 ? 0 : G > 255 ? 255 : G) * 0x100 + (B < 0 ? 0 : B > 255 ? 255 : B)).toString(16).slice(1);
}
export function lightenColor(color, percent) {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt;
const G = ((num >> 8) & 0x00FF) - amt;
const B = (num & 0x0000FF) - amt;
return '#' + (0x1000000 + (R < 0 ? 0 : R > 255 ? 255 : R) * 0x10000 + (G < 0 ? 0 : G > 255 ? 255 : G) * 0x100 + (B < 0 ? 0 : B > 255 ? 255 : B)).toString(16).slice(1);
const R = (num >> 16) + amt;
const G = ((num >> 8) & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
}
export function oppositeColor(color) {
const num = parseInt(color.replace('#', ''), 16);
+20 -221
View File
@@ -1,5 +1,4 @@
import { VERMINE } from "./config.mjs";
import { getActorSkillScore, updateActorSkillScore } from "./functions.mjs";
/**
* Handles combat-related dice rolls for Vermine2047.
@@ -28,7 +27,8 @@ export class VermineFight {
*/
async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) {
// Use d10 as per Vermine2047 official rules
const dicePool = (params.spleen != undefined || params.purpose != undefined) ? '5' : '4';
const basePool = Math.max(skill, 1)
const dicePool = (params.spleen != undefined || params.purpose != undefined) ? String(basePool + 1) : String(basePool)
const difficulty = params.difficulty || 7; // Default difficulty
const r = new Roll(dicePool + `d10`);
let diceString = '';
@@ -167,218 +167,25 @@ export class VermineFight {
// data injected to char data
static previousValues = {
dicePool: 4,
skills: VERMINE.skillsList,
cskills: VERMINE.cskills,
skills: [],
cskills: [],
cephalic: false,
achievementReroll: VERMINE.achievementReroll,
conservationReroll: VERMINE.conservationReroll
achievementReroll: 0,
conservationReroll: 0
};
static rollerTemplate = 'systems/vermine2047/templates/fight.html';
static CombatResultTemplate = 'systems/vermine2047/templates/fight-result.html';
static async chatMessageHandler(message, html, data) {
// console.log("accès au fin du fin", message._id);
// sélection du dé actif
html.on("click", '.confrontation .die.d10', event => {
const diceResult = parseInt($(event.target).html(), 10);
html.find('.confrontation .die.d10').removeClass('active');
$(event.target).addClass('active');
});
// sélection des dés d'accomplissement (achievement = successes)
html.on("click", '.confrontation .add-to-achievement', event => {
const diceResult = parseInt(html.find('.confrontation .die.d10.active').html(), 10);
html.find('.confrontation .die.d10.active').removeClass('min').addClass('max');
});
// sélection des dés de conservation
html.on("click", '.confrontation .add-to-conservation', event => {
const diceResult = parseInt(html.find('.confrontation .die.d10.active').html(), 10);
html.find('.confrontation .die.d10.active').removeClass('max').addClass('min');
});
// reset de la sélection des pools
html.on("click", '.confrontation .reset', event => {
html.find('.confrontation .die.d10')
.removeClass('max')
.removeClass('min');
});
// résolution de la confrontation
// Note: With d10 system, we count successes (dice >= difficulty) instead of summing
html.on("click", '.confrontation .resolve', async event => {
let achievementDice = 0;
let conservationDice = 0;
let achievementBasis = 0;
let conservationBasis = 0;
const difficulty = 7; // Default difficulty for legacy compatibility
// Count successes (dice >= difficulty) for achievement
html.find('.confrontation .die.d10.max').each(function (index) {
const value = parseInt($(this).html(), 10);
if (value >= difficulty) {
achievementDice++;
}
});
// Count successes (dice >= difficulty) for conservation
html.find('.confrontation .die.d10.min').each(function (index) {
const value = parseInt($(this).html(), 10);
if (value >= difficulty) {
conservationDice++;
}
});
// saisie des résultats
achievementBasis = html.find('td.achievement-result').data('achievement-basis');
html.find('td.achievement-result').data('achievement-value', achievementDice);
html.find('td.achievement-result').html(achievementBasis + achievementDice);
conservationBasis = html.find('td.conservation-result').data('conservation-basis');
html.find('td.conservation-result').data('conservation-value', conservationDice);
html.find('td.conservation-result').html(conservationBasis + conservationDice);
// calcul des marges (now based on successes, not sum)
const achievementMargin = achievementBasis + achievementDice - parseInt(html.find('td.adv-achievement-result').html(), 10);
const conservationMargin = conservationBasis + conservationDice - parseInt(html.find('td.adv-conservation-result').html(), 10);
html.find('td.achievement-margin').html(achievementMargin);
html.find('td.conservation-margin').html(conservationMargin);
});
// fin de la résolution de la confrontation
}
static async chatListeners(html) {
// supprime le masquage des résultats du dé
html.off("click", ".dice-roll");
}
/**
* main class function
* @returns
*/
static async ui(externalData = {}) {
let actor = {};
// get the actor
try {
actor = game.user.character;
} catch (e) {
throw ("Aucun personnage défini !");
}
if (actor == null && externalData.speakerId != undefined && externalData.speakerId != null) {
// on récupère le speakerId, et de là l'objet actor
actor = game.actors.get(externalData.speakerId);
VermineFight.previousValues['speakerName'] = actor.name;
VermineFight.previousValues['speakerImg'] = actor.img;
} else {
VermineFight.previousValues['speakerName'] = "Anonyme";
}
// get the data
let charData = (externalData) => {
let o = Object.assign({ _template: VermineFight.rollerTemplate }, { ...VermineFight.previousValues, ...externalData });
return o;
};
let data = charData(externalData);
console.log(data);
// render template
let html = await foundry.applications.handlebars.renderTemplate(data._template, data);
let ui = new Dialog({
title: game.i18n.localize("VERMINE.FightTool"),
content: html,
buttons: {
roll: {
label: game.i18n.localize('VERMINE.Roll4Fight'),
callback: (html) => {
let form = html.find('#dice-pool-form');
if (!form[0].checkValidity()) {
throw "Invalid Data";
}
let enemyAchievement, enemyConservation, skillKey, skill = 5, enemySkill, params = {};
form.serializeArray().forEach(e => {
switch (e.name) {
case "skill":
case "cephalic":
if (e.value !== '') {
skillKey = e.value;
}
break;
case "skill-score":
skill = +e.value;
break;
case "specialization":
params.specialization = true;
break;
case "usure":
params.usure = +e.value;
break;
case "trait":
params.trait = +e.value;
break;
case "purpose":
params.purpose = true;
break;
case "spleen":
params.spleen = true;
break;
case "adv-skill":
enemySkill = +e.value;
break;
case "achievement":
enemyAchievement = +e.value;
break;
case "conservation":
enemyConservation = +e.value;
break;
}
});
// prise en compte de l'usure sur la feuille de perso
if (params.usure != undefined) {
const newSpentScore = getActorSkillScore(actor, skillKey, 'spent') + params.usure;
console.log(newSpentScore);
updateActorSkillScore(actor, skillKey, 'spent', newSpentScore);
}
return VermineFight.get().performTest(enemyAchievement + enemySkill, enemyConservation + enemySkill, skillKey, skill, params, actor);
}
},
cancel: {
label: game.i18n.localize('Close'),
callback: () => { }
}
},
render: function (h) {
h.on("change", 'select[name="skill"]', event => {
const skillLabel = $(event.target).val();
const currentSkillScore = getActorSkillScore(actor, skillLabel) - getActorSkillScore(actor, skillLabel, 'spent');
if (parseInt(currentSkillScore, 10) >= 0) {
h.find('input#skillScore').val(currentSkillScore);
}
});
}
}, { width: 601, height: 'fit-content' });
ui.render(true);
return ui;
}
}
export class VermineCombat extends Combat {
_encounterCheck() {
console.log('encounter combat object', this);
// encounter check
}
async rollInitiative(ids, formula = undefined, messageOptions = {}) {
console.log(`${game.system.title} | Combat.rollInitiative()`, ids, formula, messageOptions);
// roll initiative
return super.rollInitiative(ids, formula, messageOptions)
}
@@ -421,13 +228,6 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
return "systems/vermine2047/templates/combat-tracker.hbs";
}
/* -------------------------------------------- */
get template() {
return "systems/vermine2047/templates/combat-tracker.hbs";
}
async getData(options) {
const context = await super.getData(options);
@@ -441,7 +241,9 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
activateListeners(html) {
super.activateListeners(html);
html.find("[data-attitude]").click(this._setStatut.bind(this));
html.querySelectorAll("[data-attitude]").forEach(el => {
el.addEventListener("click", this._setStatut.bind(this));
});
}
/**
@@ -465,23 +267,20 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
}
let actor = await game.actors.get(combatant.actorId)
console.log(actor, combatant)
// combatant attitude set
}
}
export class VermineCombatant extends Combatant {
constructor(data, context) {
super(data, context);
this.setDefaultAttitude();
}
setDefaultAttitude() {
this.attitude = this.token.flags.world?.attitude || "active"
super(data, context)
if (this.token) {
this.attitude = this.token.flags.world?.attitude || "active"
} else {
this.attitude = "active"
}
}
_getInitiativeFormula() {
return String(CONFIG.Combat.initiative.formula || game.system.initiative);
return String(CONFIG.Combat.initiative.formula || game.system.initiative)
}
}
+28 -30
View File
@@ -1,47 +1,45 @@
import { VERMINE } from './config.mjs'
/**
* renvoie le score d'une compétence d'un actor existant
* @param {VermineActor}
* @return {number||null} Data for rendering or null
* Returns the score of a skill for an actor.
* Skills are stored as a flat object in actor.system.skills: { [key]: { value, spent, label, category } }
* @param {VermineActor} actor
* @param {string} skillLabel - The localized label of the skill to find
* @param {"value"|"spent"} [property="value"] - Which property to return
* @returns {number|null} The skill score or null if not found
*/
export function getActorSkillScore(actor, skillLabel, property = "value") {
let returnedValue = null;
const skills = actor.system?.skills;
if (!skills) return null;
for(let i in actor.system.skills){
for(let j in actor.system.skills[i].data){
if (actor.system.skills[i].data[j].label == skillLabel){
returnedValue = actor.system.skills[i].data[j][property];
}
for (const key of Object.keys(skills)) {
if (skills[key].label === skillLabel) {
return skills[key][property] ?? null;
}
}
return returnedValue;
return null;
}
/**
* met à jour le score d'une compétence d'un actor existant
* @param {VermineActor}
* @return {boolean} bool
* Updates the score of a skill for an actor.
* @param {VermineActor} selectedActor
* @param {string} skillLabel - The localized label of the skill to update
* @param {"value"|"spent"} [property="value"] - Which property to update
* @param {number} updatedValue - The new value
* @returns {boolean} Whether the update was successful
*/
export function updateActorSkillScore(selectedActor, skillLabel, property = "value", updatedValue) {
try {
let updated = false;
// on recherche le label parmi les compétences
for (let st in selectedActor.system.skills){
for (let s in selectedActor.system.skills[st].data){
if (selectedActor.system.skills[st].data[s].label == skillLabel){
selectedActor.system.skills[st].data[s][property] = updatedValue; // printing the new value
const systemSkillKey = `system.skills.${st}.data.${s}.${property}`;
selectedActor.update({[systemSkillKey]:updatedValue }); // updating actor's data
updated = true;
}
const skills = selectedActor.system?.skills;
if (!skills) return false;
for (const key of Object.keys(skills)) {
if (skills[key].label === skillLabel) {
skills[key][property] = updatedValue;
selectedActor.update({ [`system.skills.${key}.${property}`]: updatedValue });
return true;
}
}
return updated;
} catch(e){
return false;
} catch (e) {
return false;
}
}
+6 -6
View File
@@ -124,7 +124,7 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('threatLevel', function (property, level, options) {
if (level < 1 || level > 4)
return "";
let levelData = CONFIG.VERMINE.ThreatLevels[level];
let levelData = CONFIG.VERMINE.npcThreatLevels[level];
if (property == 'label') {
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
} else {
@@ -136,7 +136,7 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('experienceLevel', function (property, level, options) {
if (level < 1 || level > 4)
return "";
let levelData = CONFIG.VERMINE.ExperienceLevels[level];
let levelData = CONFIG.VERMINE.npcExperienceLevels[level];
if (property == 'label') {
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
} else {
@@ -148,7 +148,7 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('roleLevel', function (property, level, options) {
if (level < 1 || level > 4)
return "";
let levelData = CONFIG.VERMINE.RoleLevels[level];
let levelData = CONFIG.VERMINE.npcRoleLevels[level];
if (property == 'label') {
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
} else {
@@ -160,7 +160,7 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('patternLevel', function (property, level, options) {
if (level < 1 || level > 4)
return "";
let levelData = CONFIG.VERMINE.PatternLevels[level];
let levelData = CONFIG.VERMINE.creaturePatternLevels[level];
if (property == 'label') {
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
} else {
@@ -172,7 +172,7 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('sizeLevel', function (property, level, options) {
if (level < 1 || level > 4)
return "";
let levelData = CONFIG.VERMINE.SizeLevels[level];
let levelData = CONFIG.VERMINE.creatureSizeLevels[level];
if (property == 'label') {
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
} else {
@@ -184,7 +184,7 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('packLevel', function (property, level, options) {
if (level < 0 || level > 3)
return "";
let levelData = CONFIG.VERMINE.PackLevels[level];
let levelData = CONFIG.VERMINE.creaturePackLevels[level];
if (property == 'label') {
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
} else {
+15 -9
View File
@@ -1,6 +1,5 @@
import RollDialog from "./dialogs/rollDialog.mjs";
import { initUserDice } from "./dice3d.mjs";
import { DiceSystem } from '../../../../modules/dice-so-nice/api.js';
import { VermineUtils } from "./roll.mjs";
import { registerTours } from "./tour.mjs";
@@ -10,6 +9,9 @@ export const registerHooks = function () {
*/
CONFIG.debug.hooks = false;
Hooks.once('diceSoNiceReady', async (dice3d) => {
const dsnModule = game.modules.get('dice-so-nice');
if (!dsnModule?.active) return;
const { DiceSystem } = await import(`/modules/${dsnModule.id}/api.js`);
const vermineSystem = new DiceSystem('Vermine2047', 'Vermine 2047', "preferred", 'totem')
dice3d.addSystem(vermineSystem);
@@ -43,7 +45,7 @@ export const registerHooks = function () {
if (rerollTitle) {
rerollTitle.addEventListener("click", () => { html.querySelector(".reroll").classList.toggle('visible') })
}
if (message.author?._id != game.user._id || !game.user.isGM) {
if (message.author?._id !== game.user._id && !game.user.isGM) {
// désactiver les inputs pour les joueurs non-auteurs du message
html.querySelectorAll("input").forEach(inp => inp.disabled = true);
//cacher le boutton reroll
@@ -61,17 +63,21 @@ export const registerHooks = function () {
}
})
Hooks.once("ready", async () => {
console.info("Vermine 2047 | System Initialized.");
// System initialized
//await registerTours();
});
// changement de la pause
Hooks.on("renderPause", async function () {
if ($("#pause").attr("class") !== "paused") return;
$(".paused img").attr("src", 'systems/vermine2047/assets/images/ui/vermine_pause.webp');
$(".paused img").css({ "opacity": 1 });
$("#pause.paused figcaption").text("Communauté endormie...");
const pauseEl = document.getElementById("pause");
if (!pauseEl || pauseEl.className !== "paused") return;
pauseEl.querySelectorAll("img").forEach(img => {
img.src = 'systems/vermine2047/assets/images/ui/vermine_pause.webp';
img.style.opacity = 1;
});
const caption = pauseEl.querySelector("figcaption");
if (caption) caption.textContent = "Communauté endormie...";
});
/**
@@ -107,7 +113,7 @@ export const registerHooks = function () {
/* -------------------------------------------- */
Hooks.on("preCreateActor", function (actor) {
console.log('pre create actor', actor.img);
// pre create actor
if (actor.img == "icons/svg/mystery-man.svg") {
actor.updateSource({ "img": `systems/vermine2047/assets/icons/actors/${actor.type}.webp` });
}
@@ -138,7 +144,7 @@ export const registerHooks = function () {
if (game.user.isGM) {
let combatant = (game.combat.combatant) ? game.combat.combatant.actor : "";
console.log('update combat', game.combat);
// update combat
/*if (combatant.type == "marker" && combatant.system.settings.general.isCounter == true) {
let step = (!combatant.system.settings.general.counting) ? -1 : combatant.system.settings.general.counting;
+52 -54
View File
@@ -28,7 +28,8 @@ export class VermineUtils {
skillCategory = null,
keepTotem = null,
skillLevel = null,
hasSpecialty = false
hasSpecialty = false,
handicap = 0
}) {
// Validate inputs
if (!actor) {
@@ -100,10 +101,13 @@ export class VermineUtils {
NoD++; // Cancel the decrement for adapted
} else if (keepTotem === 'adapted' && totems.human) {
modFormula = `(1D10cs>=${adjustedDifficulty}[adapted_${safeUserName}]*2)`;
NoD++; // Cancel the decrement for human
NoD++; // Cancel the decrement for human
}
}
// Apply handicap (wounds/maluses reduce dice pool)
NoD = Math.max(1, NoD - handicap)
// Build base formula
const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`;
@@ -364,67 +368,61 @@ export class VermineUtils {
* @param {HTMLElement} html - The HTML element containing chat events.
*/
static async chatListenners(html) {
// Ensure html is a jQuery object
const $html = $(html);
// Get reroll count
const rerollCountElement = $html.find('#allowed_reroll')[0];
const rerollCountElement = html.querySelector('#allowed_reroll');
const rerollCount = rerollCountElement?.innerText;
// Enable/disable rerolls based on count
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
// Disable rerolls for all dice
$html.find('.die').each(function() {
this.classList.remove("rerollable");
});
} else {
// Enable rerolls for all dice
$html.find('.die').each(function() {
this.classList.add("rerollable");
});
}
const dieClass = !rerollCount || parseInt(rerollCount, 10) < 1 ? 'remove' : 'add';
html.querySelectorAll('.die').forEach(el => el.classList[dieClass]("rerollable"));
// Add click event for rerollable dice
$html.find('.rerollable').click(async (ev) => {
ev.preventDefault();
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId;
if (msgId) {
const message = await game.messages.get(msgId);
await VermineUtils.onReroll(message, ev);
}
html.querySelectorAll('.rerollable').forEach(el => {
el.addEventListener('click', async (ev) => {
ev.preventDefault();
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId;
if (msgId) {
const message = await game.messages.get(msgId);
await VermineUtils.onReroll(message, ev);
}
});
});
// Update granted reroll label
$html.find("#effort-reroll").change(ev => {
const label = $html.find("#granted-reroll")[0];
if (label) {
label.innerText = ev.currentTarget.value;
}
});
const effortReroll = html.querySelector("#effort-reroll");
if (effortReroll) {
effortReroll.addEventListener('change', ev => {
const label = html.querySelector("#granted-reroll");
if (label) {
label.innerText = ev.currentTarget.value;
}
});
}
// Add click event for granting rerolls
$html.find("button.grant-reroll").click(async (ev) => {
const grantedRerollElement = $html.find('#granted-reroll')[0];
const allowedRerollElement = $html.find("#allowed_reroll")[0];
if (grantedRerollElement && allowedRerollElement) {
allowedRerollElement.innerText = grantedRerollElement.innerText;
}
const mesEl = ev.currentTarget.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId;
if (messageId) {
// Hide reroll grant area
ev.currentTarget.closest('.reroll-from-effort').style.display = "none";
const rollMessage = ev.currentTarget.closest(".vermine-roll-message");
if (rollMessage) {
const content = rollMessage.outerHTML;
const message = await game.messages.get(messageId);
await message.update({ content: content });
html.querySelectorAll("button.grant-reroll").forEach(el => {
el.addEventListener('click', async (ev) => {
const grantedRerollElement = html.querySelector('#granted-reroll');
const allowedRerollElement = html.querySelector("#allowed_reroll");
if (grantedRerollElement && allowedRerollElement) {
allowedRerollElement.innerText = grantedRerollElement.innerText;
}
}
const mesEl = ev.currentTarget.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId;
if (messageId) {
ev.currentTarget.closest('.reroll-from-effort').style.display = "none";
const rollMessage = ev.currentTarget.closest(".vermine-roll-message");
if (rollMessage) {
const content = rollMessage.outerHTML;
const message = await game.messages.get(messageId);
await message.update({ content: content });
}
}
});
});
}
@@ -448,10 +446,10 @@ export class VermineUtils {
blind = true;
// Falls through
case "gmroll": // GM + rolling player
whisper = this.getUsers(user => user.isGM);
whisper = game.users?.filter(user => user.isGM) || [];
break;
case "roll": // Everybody
whisper = this.getUsers(user => user.active);
whisper = game.users?.filter(user => user.active) || [];
break;
case "selfroll":
whisper = [game.user.id];
+1 -1
View File
@@ -10,7 +10,7 @@ export const registerSettings = function () {
"2": "Cauchemar",
"3": "Apocalypse"
},
default: 'e',
default: '1',
onChange: value => {
let el = document.querySelector('.game-mode');
el.id = 'game-mode-' + game.settings.get('vermine2047', 'game-mode')
+6 -4
View File
@@ -139,13 +139,15 @@ class WelcomeTour extends VermineTour {
}
export async function registerTours() {
game.tours.register("vermine2047", "welcome", new WelcomeTour());
$(document).on("click", "#chat-log #vermine-tour-chat-button", (el) => {
const tour = game.tours.get("vermine2047.welcome");
tour == null ? void 0 : tour.start();
document.addEventListener("click", (el) => {
if (el.target.closest("#chat-log #vermine-tour-chat-button")) {
const tour = game.tours.get("vermine2047.welcome");
tour?.start();
}
});
if (game.settings.get("vermine2047", "first-run-tips-shown"))
return;
console.log("Posting first-start messages...");
// Posting first-start messages
const gms = ChatMessage.getWhisperRecipients("GM");
ChatMessage.implementation;
ChatMessage.create({