/* -------------------------------------------- */ import { PegasusCombat } from "./pegasus-combat.js"; import { PegasusCommands } from "./pegasus-commands.js"; import { PegasusActorCreate } from "./pegasus-create-char.js"; import { PegasusRollDialog } from "./pegasus-roll-dialog.js"; /* -------------------------------------------- */ const __level2Dice = ["d0", "d4", "d6", "d8", "d10", "d12"] const __name2DiceValue = { "0": 0, "d0": 0, "d4": 4, "d6": 6, "d8": 8, "d10": 10, "d12": 12 } const __dice2Level = { "d0": 0, "d4": 1, "d6": 2, "d8": 3, "d10": 4, "d12": 5 } const __rangeKeyToText = { notapplicable: "N/A", touch: "Self Only", touchself: "Touch/Self", tz: "Threat Zone", close: "Close", medium: "Medium", long: "Long", extreme: "Extreme", sight: "Lineof Sight", tz_close: "TZ/Close", close_medium: "Close/Medium", medium_long: "Medium/Long", long_extreme: "Long/Extreme" } /* -------------------------------------------- */ export class PegasusUtility { /* -------------------------------------------- */ static async init() { Hooks.on('renderChatLog', (log, html, data) => PegasusUtility.chatListeners(html)) Hooks.on('targetToken', (user, token, flag) => PegasusUtility.targetToken(user, token, flag)) Hooks.on('renderSidebarTab', (app, html, data) => PegasusUtility.addDiceRollButton(app, html, data)) Hooks.on("getCombatTrackerEntryContext", (html, options) => { PegasusUtility.pushInitiativeOptions(html, options); }); Hooks.on("dropCanvasData", (canvas, data) => { PegasusUtility.dropItemOnToken(canvas, data) }); this.rollDataStore = {} this.defenderStore = {} this.diceList = []; this.diceFoundryList = []; this.optionsDiceList = "" this.lastRoleEffectProcess = Date.now() this.buildDiceLists() PegasusCommands.init() Handlebars.registerHelper('count', function (list) { return (list) ? list.length : 0; }) Handlebars.registerHelper('includes', function (array, val) { return array.includes(val); }) Handlebars.registerHelper('upper', function (text) { return text.toUpperCase(); }) Handlebars.registerHelper('lower', function (text) { return text.toLowerCase() }) Handlebars.registerHelper('upperFirst', function (text) { if (typeof text !== 'string') return text return text.charAt(0).toUpperCase() + text.slice(1) }) Handlebars.registerHelper('notEmpty', function (list) { return list.length > 0; }) Handlebars.registerHelper('mul', function (a, b) { return parseInt(a) * parseInt(b); }) Handlebars.registerHelper('add', function (a, b) { return parseInt(a) + parseInt(b); }); Handlebars.registerHelper('sub', function (a, b) { return parseInt(a) - parseInt(b); }) Handlebars.registerHelper('getDice', function (a) { return PegasusUtility.getDiceFromLevel(a) }) } /* -------------------------------------------- */ static initGenericRoll() { let rollData = PegasusUtility.getBasicRollData() rollData.alias = "Dice Pool Roll", rollData.mode = "generic" rollData.title = `Dice Pool Roll` rollData.img = "icons/dice/d12black.svg" rollData.isGeneric = true rollData.diceList = PegasusUtility.getDiceList() rollData.dicePool = [] rollData.traumaState = "none" return rollData } /* -------------------------------------------- */ static async addDiceRollButton(app, html, data) { if (app.tabName !== 'chat') return let $chat_form = html.find('#chat-form') const template = 'systems/fvtt-pegasus-rpg/templates/chat-roll-button.html' renderTemplate(template, {}).then(c => { if (c.length > 0) { let $content = $(c) $chat_form.before($content) $content.find('#pegasus-chat-roll-button').on('click', async event => { event.preventDefault() let rollData = PegasusUtility.initGenericRoll() rollData.isChatRoll = true let rollDialog = await PegasusRollDialog.create(undefined, rollData) rollDialog.render(true) }) } }) } /* -------------------------------------------- */ static pushInitiativeOptions(html, options) { options.push({ name: "Apply -10", condition: true, icon: '', callback: target => { PegasusCombat.decInitBy10(target.data('combatant-id'), -10); } }) } /* -------------------------------------------- */ static getRangeText(rangeKey) { return __rangeKeyToText[rangeKey] || "N/A" } /* -------------------------------------------- */ static getDiceList() { return [{ key: "d4", level: 1, img: "systems/fvtt-pegasus-rpg/images/dice/d4.webp" }, { key: "d6", level: 2, img: "systems/fvtt-pegasus-rpg/images/dice/d6.webp" }, { key: "d8", level: 3, img: "systems/fvtt-pegasus-rpg/images/dice/d8.webp" }, { key: "d10", level: 4, img: "systems/fvtt-pegasus-rpg/images/dice/d10.webp" }, { key: "d12", level: 5, img: "systems/fvtt-pegasus-rpg/images/dice/d12.webp" }] } /* -------------------------------------------- */ static buildDicePool(name, level, mod = 0, effectName = undefined) { let dicePool = [] let diceKey = PegasusUtility.getDiceFromLevel(level) let diceList = diceKey.split(" ") for (let myDice of diceList) { myDice = myDice.trim() let newDice = { name: name, key: myDice, level: PegasusUtility.getLevelFromDice(myDice), mod: mod, effect: effectName, img: `systems/fvtt-pegasus-rpg/images/dice/${myDice}.webp` } dicePool.push(newDice) mod = 0 // Only first dice has modifier } return dicePool } /* -------------------------------------------- */ static updateEffectsBonusDice(rollData) { let newDicePool = rollData.dicePool.filter(dice => dice.name != "effect-bonus-dice") for (let effect of rollData.effectsList) { if (effect && effect.applied && effect.type == "effect" && effect.effect && effect.effect.system.bonusdice) { newDicePool = newDicePool.concat(this.buildDicePool("effect-bonus-dice", effect.effect.system.effectlevel, 0, effect.effect.name)) } if (effect && effect.applied && effect.type == "effect" && effect.value && effect.isdynamic) { newDicePool = newDicePool.concat(this.buildDicePool("effect-bonus-dice", effect.value, 0, effect.name)) } } rollData.dicePool = newDicePool } /* -------------------------------------------- */ static updateHindranceBonusDice(rollData) { let newDicePool = rollData.dicePool.filter(dice => dice.name != "effect-hindrance") for (let hindrance of rollData.effectsList) { if (hindrance && hindrance.applied && (hindrance.type == "hindrance" || (hindrance.type == "effect" && hindrance.effect?.system?.hindrance))) { newDicePool = newDicePool.concat(this.buildDicePool("effect-hindrance", (hindrance.value) ? hindrance.value : hindrance.effect.system.effectlevel, 0, hindrance.name)) } } rollData.dicePool = newDicePool } /* -------------------------------------------- */ static updateArmorDicePool(rollData) { let newDicePool = rollData.dicePool.filter(dice => dice.name != "armor-shield") for (let armor of rollData.armorsList) { if (armor.applied) { newDicePool = newDicePool.concat(this.buildDicePool("armor-shield", armor.value, 0)) } } newDicePool = newDicePool.filter(dice => dice.name != "vehicle-shield") for (let shield of rollData.vehicleShieldList) { if (shield.applied) { newDicePool = newDicePool.concat(this.buildDicePool("vehicle-shield", shield.value, 0)) } } console.log(">>>>Dicepoool", newDicePool) rollData.dicePool = newDicePool } /* -------------------------------------------- */ static updateDamageDicePool(rollData) { if (rollData.isDamage) { let newDicePool = rollData.dicePool.filter(dice => dice.name != "damage") for (let weapon of rollData.weaponsList) { if (weapon.applied && weapon.type == "damage") { newDicePool = newDicePool.concat(this.buildDicePool("damage", weapon.value, 0)) } } for (let weapon of rollData.vehicleWeapons) { if (weapon.applied) { newDicePool = newDicePool.concat(this.buildDicePool("damage", weapon.value, 0)) } } rollData.dicePool = newDicePool } } /* -------------------------------------------- */ static updateStatDicePool(rollData) { let newDicePool = rollData.dicePool.filter(dice => dice.name != "stat") let statDice = rollData.dicePool.find(dice => dice.name == "stat") if (statDice.level > 0) { newDicePool = newDicePool.concat(this.buildDicePool("stat", rollData.statDicesLevel, statDice.mod)) } if (rollData.vehicleStat) { newDicePool = rollData.dicePool.filter(dice => dice.name != "vehiclestat") if (rollData.vehicleStat.currentlevel > 0) { newDicePool = newDicePool.concat(this.buildDicePool("vehiclestat", rollData.vehicleStat.currentlevel, 0)) } rollData.dicePool = newDicePool } } /* -------------------------------------------- */ static updateSpecDicePool(rollData) { let newDicePool = rollData.dicePool.filter(dice => dice.name != "spec") if (rollData.specDicesLevel > 0) { newDicePool = newDicePool.concat(this.buildDicePool("spec", rollData.specDicesLevel, 0)) } rollData.dicePool = newDicePool } /* -------------------------------------------- */ static addDicePool(rollData, diceKey, level) { let newDice = { name: "dice-click", key: diceKey, level: level, img: `systems/fvtt-pegasus-rpg/images/dice/${diceKey}.webp` } rollData.dicePool.push(newDice) } /*-------------------------------------------- */ static removeFromDicePool(rollData, diceIdx) { let toRemove = rollData.dicePool[diceIdx] if (toRemove && toRemove.name != "spec" && toRemove.name != "stat" && toRemove.name != "damage") { let newDicePool = [] for (let i = 0; i < rollData.dicePool.length; i++) { if (i != diceIdx) { newDicePool.push(rollData.dicePool[i]) } } rollData.dicePool = newDicePool if (toRemove.name == "effect-bonus-dice") { for (let effect of rollData.effectsList) { if (effect.effect.name == toRemove.effect && effect.applied) { effect.applied = false //Remove the effect } } } } } /*-------------------------------------------- */ static getSpecs() { return this.specs; } /* -------------------------------------------- */ static async ready() { const specs = await PegasusUtility.loadCompendium("fvtt-pegasus-rpg.specialisations"); this.specs = specs.map(i => i.toObject()); if (game.user.isGM) { Hooks.on('sightRefresh', (app, html, data) => PegasusUtility.refreshSightForEffect(app, html, data)) } } /* -------------------------------------------- */ static async addItemDropToActor(actor, item) { console.log("ITEM DROPPED", actor, item) actor.preprocessItem("none", item, false) let chatData = { user: game.user.id, rollMode: game.settings.get("core", "rollMode"), whisper: [game.user.id].concat(ChatMessage.getWhisperRecipients('GM')), content: `
Do you want to re-roll your last roll ?
", buttons: { one: { icon: '', label: "Cancel", callback: () => d.close() }, two: { icon: '', label: "Reroll", callback: () => PegasusUtility.momentumReroll(actorId) } }, default: "Reroll", }) d.render(true) } /* -------------------------------------------- */ static async rollPegasus(rollData) { let actor = game.actors.get(rollData.actorId) let diceFormulaTab = [] for (let dice of rollData.dicePool) { let level = dice.level diceFormulaTab.push(this.getFoundryDiceFromLevel(level)) } let diceFormula = '{' + diceFormulaTab.join(', ') + '}kh + ' + (rollData.stat?.mod || 0) // Performs roll let myRoll = rollData.roll if (!myRoll || rollData.rerollHero || rollData.rerollMomentum) { // New rolls only of no rerolls myRoll = new Roll(diceFormula).roll({ async: false }) await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode")) rollData.roll = myRoll } // Final score and keep data rollData.finalScore = myRoll.total if (rollData.damages) { let dmgFormula = this.getFoundryDiceFromLevel(rollData.damages.value) let dmgRoll = new Roll(dmgFormula).roll({ async: false }) await this.showDiceSoNice(dmgRoll, game.settings.get("core", "rollMode")) rollData.dmgResult = dmgRoll.total } this.createChatWithRollMode(rollData.alias, { content: await renderTemplate(`systems/fvtt-pegasus-rpg/templates/chat-generic-result.html`, rollData) }); // Init stuf if (rollData.isInit) { let combat = game.combats.get(rollData.combatId) combat.updateEmbeddedDocuments("Combatant", [{ _id: rollData.combatantId, initiative: rollData.finalScore }]) } // Stun specific -> Suffer a stun level when dmg-res for character if (rollData.subKey && rollData.subKey == "dmg-res") { actor.modifyStun(+1) } if (rollData.isVehicleStun) { actor.modifyVehicleStun(1) } //this.removeUsedPerkEffects( rollData) // Unused for now this.removeOneUseEffects(rollData) // Unused for now // And save the roll this.saveRollData(rollData) actor.lastRoll = rollData console.log("Rolldata performed ", rollData, diceFormula) } /* -------------------------------------------- */ static getDamageDice(result) { if (result < 0) return 0; return Math.floor(result / 5) + 1; } /* ------------------------- ------------------- */ static async updateRoll(rollData) { let diceResults = rollData.diceResults; let sortedRoll = []; for (let i = 0; i < 10; i++) { sortedRoll[i] = 0; } for (let dice of diceResults) { sortedRoll[dice.result]++; } let index = 0; let bestRoll = 0; for (let i = 0; i < 10; i++) { if (sortedRoll[i] > bestRoll) { bestRoll = sortedRoll[i]; index = i; } } let bestScore = (bestRoll * 10) + index rollData.bestScore = bestScore rollData.finalScore = bestScore + rollData.negativeModifier + rollData.positiveModifier this.saveRollData(rollData) this.createChatWithRollMode(rollData.alias, { content: await renderTemplate(`systems/fvtt-weapons-of-the-gods/templates/chat-generic-result.html`, rollData) }); } /* ------------------------- ------------------- */ static async rerollDice(actorId, diceIndex = -1) { let actor = game.actors.get(actorId); let rollData = actor.getRollData(); if (diceIndex == -1) { rollData.hasWillpower = actor.decrementWillpower(); rollData.roll = undefined; } else { let myRoll = new Roll("1d6").roll({ async: false }); await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode")); console.log("Result: ", myRoll); rollData.roll.dice[0].results[diceIndex].result = myRoll.total; // Patch rollData.nbStrongHitUsed++; } this.rollFraggedKingdom(rollData); } /* -------------------------------------------- */ static getUsers(filter) { return game.users.filter(filter).map(user => user.id); } /* -------------------------------------------- */ static getWhisperRecipients(rollMode, name) { switch (rollMode) { case "blindroll": return this.getUsers(user => user.isGM); case "gmroll": return this.getWhisperRecipientsAndGMs(name); case "selfroll": return [game.user.id]; } return undefined; } /* -------------------------------------------- */ static getWhisperRecipientsAndGMs(name) { let recep1 = ChatMessage.getWhisperRecipients(name) || []; return recep1.concat(ChatMessage.getWhisperRecipients('GM')); } /* -------------------------------------------- */ static blindMessageToGM(chatOptions) { let chatGM = duplicate(chatOptions); chatGM.whisper = this.getUsers(user => user.isGM); chatGM.content = "Blinde message of " + game.user.name + "Are you sure to remove this Item ?"; let buttons = { delete: { icon: '', label: "Yes, remove it", callback: () => { actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]) li.slideUp(200, () => actorSheet.render(false)); } }, cancel: { icon: '', label: "Cancel" } } msgTxt += "
"; let d = new Dialog({ title: "Confirm removal", content: msgTxt, buttons: buttons, default: "cancel" }); d.render(true); } /* -------------------------------------------- */ static checkIsVehicleCrew(actorId) { let vehicles = game.actors.filter( actor=> actor.type == "vehicle") || [] for(let vehicle of vehicles) { console.log("Checking", vehicle.name) if ( vehicle.inCrew(actorId) ) { return vehicle } } return false } /* -------------------------------------------- */ static async getRelevantTokens() { if (!_token) { return } let tokens = canvas.tokens.placeables.filter(token => token.document.disposition == 1) for (let token of tokens) { console.log("Parsing tokens", token.name) let dist = canvas.grid.measureDistances( [{ ray: new Ray(_token.center, token.center) }], { gridSpaces: false }) if (dist && dist[0] && dist[0] > 0) { console.log(" Friendly Tokens at : ", token.name, dist / canvas.grid.grid.options.dimensions.distance) } let visible = canvas.effects.visibility.testVisibility(token.center, { object: _token }) if (visible && dist[0] > 0) { this.glowToken(token) } console.log(" Visible!", visible) } } /* -------------------------------------------- */ static async processTactician() { // Tactician management let toApply = {} let tacticianTokens = canvas.tokens.placeables.filter(token => token.actor.isTactician() && !token.document.hidden) for (let token of tacticianTokens) { token.refresh() let friends = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && newToken.document.disposition == token.document.disposition) for (let friend of friends) { if (friend.actor.id != token.actor.id) { let existing = toApply[friend.actor.id] || { actor: friend.actor, add: false, level: 0, names: [] } let visible = canvas.effects.visibility.testVisibility(friend.center, { object: token }) console.log("parse visible TACTICIAN : ", visible, token.name, friend.name) if (visible) { existing.add = true existing.level += token.actor.getRoleLevel() existing.names.push(token.actor.name) } toApply[friend.actor.id] = existing } } } for (let id in toApply) { let applyDef = toApply[id] let hasBonus = applyDef.actor.hasTacticianBonus() if (applyDef.add) { if (!hasBonus) { applyDef.actor.addTacticianEffect(applyDef.names.toString(), applyDef.level) } else if (applyDef.level != hasBonus.system.effectlevel) { await applyDef.actor.removeTacticianEffect() applyDef.actor.addTacticianEffect(applyDef.names.toString(), applyDef.level) } } else if (hasBonus) { applyDef.actor.removeTacticianEffect() } } //Delete all effects if no more tacticians (ie deleted case) if (tacticianTokens.length == 0) { let allTokens = canvas.tokens.placeables.filter(token => token.actor.type == "character") for (let token of allTokens) { if (token.actor.hasTacticianBonus()) { token.actor.removeTacticianEffect() } } } } /* -------------------------------------------- */ static async processEnhancer() { // Enhancer management let toApply = {} let enhancerTokens = canvas.tokens.placeables.filter(token => token.actor.isEnhancer() && !token.document.hidden) for (let token of enhancerTokens) { token.refresh() let friends = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && newToken.document.disposition == token.document.disposition) for (let friend of friends) { if (friend.actor.id != token.actor.id) { let existing = toApply[friend.actor.id] || { actor: friend.actor, add: false, level: 0, names: [] } let visible = canvas.effects.visibility.testVisibility(friend.center, { object: token }) console.log("parse visible ENHANCER: ", visible, token.name, friend.name) if (visible) { let dist = canvas.grid.measureDistances([{ ray: new Ray(token.center, friend.center) }], { gridSpaces: false }) if (dist && dist[0] && (dist[0] / canvas.grid.grid.options.dimensions.distance) <= 5) { existing.add = true existing.level += token.actor.getRoleLevel() existing.names.push(token.actor.name) } } toApply[friend.actor.id] = existing } } } for (let id in toApply) { let applyDef = toApply[id] let hasBonus = applyDef.actor.hasEnhancerBonus() if (applyDef.add) { if (!hasBonus) { applyDef.actor.addEnhancerEffect(applyDef.names.toString(), applyDef.level) } else if (applyDef.level != hasBonus.system.effectlevel) { await applyDef.actor.removeEnhancerEffect() applyDef.actor.addEnhancerEffect(applyDef.names.toString(), applyDef.level) } } else if (hasBonus) { applyDef.actor.removeEnhancerEffect() } } // Delete all effects if no more tacticians (ie deleted case) if (enhancerTokens.length == 0) { let allTokens = canvas.tokens.placeables.filter(token => token.actor.type == "character") for (let token of allTokens) { if (token.actor.hasEnhancerBonus()) { token.actor.removeEnhancerEffect() } } } } /* -------------------------------------------- */ static async processAgitator() { // Agitator management let toApply = {} let agitatorTokens = canvas.tokens.placeables.filter(token => token.actor.isAgitator() && !token.document.hidden) for (let token of agitatorTokens) { token.refresh() if (token.document.disposition == 0) { continue } let disposition = ( token.document.disposition == -1) ? 1 : -1 let ennemies = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && newToken.document.disposition == disposition) let neutrals = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && newToken.document.disposition == 0) if (neutrals ) { ennemies = ennemies.concat(neutrals) } for (let ennemy of ennemies) { if (ennemy.actor.id != token.actor.id) { let existing = toApply[ennemy.actor.id] || { actor: ennemy.actor, add: false, level: 0, names: [] } let visible = canvas.effects.visibility.testVisibility(ennemy.center, { object: token }) if (visible) { let dist = canvas.grid.measureDistances([{ ray: new Ray(token.center, ennemy.center) }], { gridSpaces: false }) if (dist && dist[0] && (dist[0] / canvas.grid.grid.options.dimensions.distance) <= 5) { existing.add = true existing.level += token.actor.getRoleLevel() existing.names.push(token.actor.name) } } toApply[ennemy.actor.id] = existing } } } for (let id in toApply) { let applyDef = toApply[id] let hasHindrance = applyDef.actor.hasAgitatorHindrance() if (applyDef.add) { if (!hasHindrance) { applyDef.actor.addAgitatorHindrance(applyDef.names.toString(), applyDef.level) } else if (applyDef.level != hasHindrance.system.effectlevel) { await applyDef.actor.removeAgitatorHindrance() applyDef.actor.addAgitatorHindrance(applyDef.names.toString(), applyDef.level) } } else if (hasHindrance) { applyDef.actor.removeAgitatorHindrance() } } // Delete all effects if no more agtators (ie deleted case) if (agitatorTokens.length == 0) { let allTokens = canvas.tokens.placeables.filter(token => token.actor.type == "character") for (let token of allTokens) { if (token.actor.hasAgitatorHindrance()) { token.actor.removeAgitatorHindrance() } } } } /* -------------------------------------------- */ static async processRoleEffects() { // Small optimization let now = Date.now() if (now - this.lastRoleEffectProcess < 300) { return // Save some processing } this.lastRoleEffectProcess = now console.log("=========================+> Searching/Processing roles effects") await this.processTactician() await this.processEnhancer() await this.processAgitator() } /* -------------------------------------------- */ static async refreshSightForEffect() { setTimeout(500, this.processRoleEffects()) } }