/** * Handles combat-related dice rolls for Vermine2047. * Uses d10-based system with success counting as per official rules. */ export class VermineFight { /** * Performs a d10-based test according to Vermine2047 official rules. * Each die result is compared to a difficulty threshold. * Each result >= difficulty counts as 1 Success. * * @param {number} enemyAchievement - Opponent's achievement score * @param {number} enemyConservation - Opponent's conservation score * @param {string} skillKey - The skill key being tested * @param {number} skill - The skill value * @param {Object} params - Additional test parameters * @param {number} [params.difficulty=7] - Difficulty threshold (3-10) * @param {boolean} [params.spleen] - Whether to use spleen rule * @param {boolean} [params.purpose] - Whether to use purpose rule * @param {number} [params.usure] - Wear/usage modifier * @param {number} [params.trait] - Trait bonus * @param {boolean} [params.specialization] - Whether specialization applies * @param {Actor} actor - The actor performing the test * @returns {Promise} Roll data including successes count */ async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) { // Use d10 as per Vermine2047 official rules 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 = ''; let dicePoolHint = ''; let discardedRoll = false; let bonus = 0; let bonusText = '/+'; let currentSkillScore = skill; r.roll(); // dice are rolled if (params.usure != undefined) { currentSkillScore += params.usure; bonus += params.usure; } if (params.specialization != undefined) { currentSkillScore += 2; } if (params.trait != undefined) { currentSkillScore += params.trait; bonus += params.trait; } bonusText += bonus; let targetText = game.i18n.format('VERMINE.Selected') + ' : ' + game.i18n.format(skillKey) + " " + skill + bonusText; if (params.specialization != undefined) { targetText += " (S)"; } // tri par ordre croissant r.terms[0].results.sort((a, b) => a.result - b.result); if (params.purpose != undefined) { discardedRoll = r.terms[0].results.shift(); dicePoolHint = ' - ' + game.i18n.format('VERMINE.PurposeTrait'); } else if (params.spleen != undefined) { discardedRoll = r.terms[0].results.pop(); dicePoolHint = ' - ' + game.i18n.format('VERMINE.SpleenTrait'); } const discardedRollText = (discardedRoll.result != undefined) ? '
' + discardedRoll.result + '
' : ""; // Count successes (dice >= difficulty) as per Vermine2047 rules let successes = 0; for (let i = 0; i < r.terms[0].results.length; i++) { let result = r.terms[0].results[i].result; if (result >= difficulty) { successes++; } diceString += '
  • ' + result + '
  • '; } let hintText = game.i18n.format('VERMINE.ConfrontationHint'); // Build a dynamic html using the variables from above. const html = `
    ` + dicePool + `d10 ` + dicePoolHint + `
    ` + targetText + `
      ` + diceString + `
    ` + discardedRollText + `
    ` + `

    ` + hintText + `

    ` + game.i18n.format('VERMINE.success_count') + `: ` + successes + `
    `; // Check if the dice3d module exists (Dice So Nice). If it does, post a roll in that and then // send to chat after the roll has finished. If not just send to chat. if (game.dice3d) { game.dice3d.showForRoll(r).then((displayed) => { this.sendToChat(html, r, actor); }); } else { this.sendToChat(html, r, actor); }; // Return roll data for further processing with d10 success counting return { roll: r, successes: successes, difficulty: difficulty, dicePool: parseInt(dicePool, 10), total: r.total }; } async sendToChat(content, roll, actor) { let conf = { user: game.user._id, content: content, roll: roll, // sound: 'sounds/dice.wav' }; if (actor) conf.speaker = ChatMessage.getSpeaker({ actor: actor }); // Send's Chat Message to foundry, if items are missing they will appear as false or undefined and this not be rendered. ChatMessage.create(conf).then((msg) => { return msg; }); } static instance = null; static get() { if (!VermineFight.instance) VermineFight.instance = new VermineFight(); return VermineFight.instance; } // data injected to char data static previousValues = { dicePool: 4, skills: [], cskills: [], cephalic: false, achievementReroll: 0, conservationReroll: 0 }; static rollerTemplate = 'systems/vermine2047/templates/fight.html'; static CombatResultTemplate = 'systems/vermine2047/templates/fight-result.html'; } export class VermineCombat extends Combat { _encounterCheck() { // encounter check } async rollInitiative(ids, formula = undefined, messageOptions = {}) { // roll initiative return super.rollInitiative(ids, formula, messageOptions) } nextRound() { /*let combatants = this.combatants.contents for (let c of combatants) { let actor = game.actors.get( c.actorId ) actor.clearRoundModifiers() }*/ super.nextRound(); } /************************************************************************************/ startCombat() { /*let combatants = this.combatants.contents for (let c of combatants) { let actor = game.actors.get( c.actorId ) actor.storeVitaliteCombat() }*/ return super.startCombat(); } /************************************************************************************/ _onDelete() { /*let combatants = this.combatants.contents for (let c of combatants) { let actor = game.actors.get(c.actorId) actor.clearInitiative() actor.displayRecuperation() } super._onDelete()*/ } } export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { get template() { return "systems/vermine2047/templates/combat-tracker.hbs"; } async getData(options) { const context = await super.getData(options); if (!context.hasCombat) { return context; } return context; } activateListeners(html) { super.activateListeners(html); html.querySelectorAll("[data-attitude]").forEach(el => { el.addEventListener("click", this._setStatut.bind(this)); }); } /** * @description Use to put an attitude to an actor * @param {*} event */ async _setStatut(event) { event.preventDefault(); event.stopPropagation(); const btn = event.currentTarget; const attitude = btn.dataset.attitude; const li = btn.closest(".combatant"); const combat = this.viewed; const combatant = combat.combatants.get(li.dataset.combatantId); let flag = combatant.getFlag("world", "attitude"); if (flag == attitude) { await combatant.setFlag("world", "attitude", null); } else { await combatant.setFlag("world", "attitude", attitude); } let actor = await game.actors.get(combatant.actorId) // combatant attitude set } } export class VermineCombatant extends Combatant { constructor(data, context) { 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) } }