/* -------------------------------------------- */ 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)) /* Deprecated, no more used in rules Hooks.on("getCombatTrackerEntryContext", (html, options) => { PegasusUtility.pushInitiativeOptions(html, options); });*/ Hooks.once('diceSoNiceReady', (dice3d) => { dice3d.addSystem({ id: "pegasus-hindrance", name: "Hindrance Die" }, "preferred"); dice3d.addDicePreset({ type: "dh", labels: [ 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice.png' ], bumpMaps: [ 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice-empty.png', 'systems/fvtt-pegasus-rpg/images/dice/hindrance-dice.png' ], system: "pegasus-hindrance" }, "d6") }) 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) }) Handlebars.registerHelper('getStatusConfig', function (a) { let key = a + "Status" //console.log("TABE", key, game.system.pegasus.config[key] ) return game.system.pegasus.config[key] }) Handlebars.registerHelper('valueAtIndex', function (arr, idx) { return arr[idx]; }) Handlebars.registerHelper('for', function (from, to, incr, block) { let accum = ''; for (let i = from; i <= to; i += incr) accum += block.fn(i); return accum; }) Handlebars.registerHelper('isGM', function () { return game.user.isGM }) Handlebars.registerHelper('isCharacter', function (id) { return game.combat.isCharacter(id) }) } /* -------------------------------------------- */ 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) { let myDiceTrim = myDice.trim() let newDice = { name: name, key: myDiceTrim, level: PegasusUtility.getLevelFromDice(myDiceTrim), mod: mod, effect: effectName, img: `systems/fvtt-pegasus-rpg/images/dice/${myDiceTrim}.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?.applied && effect.type == "effect" && !effect.effect?.system?.hindrance && effect.effect && effect.effect.system.bonusdice) { newDicePool = newDicePool.concat(this.buildDicePool("effect-bonus-dice", effect.effect.system.effectlevel, 0, effect.effect.name)) } if (effect?.applied && effect.type == "effect" && effect.value && effect.isdynamic && !effect.effect?.system?.hindrance) { 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?.applied && (hindrance.type == "hindrance" || (hindrance.type == "effect" && hindrance.effect?.system?.hindrance))) { console.log("Adding Hindrance 1", hindrance, newDicePool) newDicePool = newDicePool.concat(this.buildDicePool("effect-hindrance", (hindrance.value) ? hindrance.value : hindrance.effect.system.effectlevel, 0, hindrance.name)) console.log("Adding Hindrance 2", newDicePool) } } 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)) if (weapon.weapon.system.extradamage) { for (let i = 0; i < weapon.weapon.system.extradamagevalue; i++) { newDicePool = newDicePool.concat(this.buildDicePool("damage", 5, 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: `
The item ${item.name} has been dropped on the actor ${actor.name}= token.x && x <= (token.x + token.width) && y >= token.y && y <= (token.y + token.height)) { const item = fromUuidSync(data.uuid) if (item == undefined) { item = this.actor.items.get(data.uuid) } let itemFull = await PegasusUtility.searchItem(item) //console.log("DROPPED DATA", data.uuid) if (game.user.isGM || token.actor.isOwner) { this.addItemDropToActor(token.actor, itemFull) } else { game.socket.emit("system.fvtt-pegasus-rpg", { name: "msg_gm_item_drop", data: { actorId: token.actor.id, itemId: itemFull.id, isPack: item.pack } }) } return } } } /* -------------------------------------------- */ static async loadCompendiumData(compendium) { const pack = game.packs.get(compendium) console.log("PACK", pack, compendium) return await pack?.getDocuments() ?? [] } /* -------------------------------------------- */ static async loadCompendium(compendium, filter = item => true) { let compendiumData = await PegasusUtility.loadCompendiumData(compendium) //console.log("Comp data", compendiumData) return compendiumData.filter(filter) } /* -------------------------------------------- */ static buildDiceLists() { let maxLevel = game.settings.get("fvtt-pegasus-rpg", "dice-max-level"); let diceList = ["0"]; let diceValues = [0]; let diceFoundryList = ["d0"]; let diceLevel = 1; let concat = ""; let concatFoundry = ""; let optionsDiceList = ''; let optionsLevel = ''; for (let i = 1; i <= maxLevel; i++) { let currentDices = concat + __level2Dice[diceLevel]; diceList.push(currentDices); diceFoundryList.push(concatFoundry + __level2Dice[diceLevel] + "x"); if (__level2Dice[diceLevel] == "d12") { concat = concat + "d12 "; concatFoundry = concatFoundry + "d12x, "; diceLevel = 1; } else { diceLevel++; } optionsDiceList += ``; optionsLevel += ``; } this.diceList = diceList; this.diceFoundryList = diceFoundryList; this.optionsDiceList = optionsDiceList; this.optionsLevel = optionsLevel; } /* -------------------------------------------- */ static getOptionsDiceList() { return this.optionsDiceList; } /* -------------------------------------------- */ static getOptionsLevel() { return this.optionsLevel; } /* -------------------------------------------- */ static computeAttackDefense(defenseRollId) { let defenseRollData = this.getRollData(defenseRollId); let attackRollData = this.getRollData(defenseRollData.linkedRollId); let defender = game.actors.get(defenseRollData.actorId); defender.processDefenseResult(defenseRollData, attackRollData); } /* -------------------------------------------- */ static applyDamage(defenseRollId) { let defenseRollData = this.getRollData(defenseRollId); let defender = game.actors.get(defenseRollData.actorId); defender.applyDamageLoss(defenseRollData.finalDamage); } /* -------------------------------------------- */ static applyNoDefense(actorId, attackRollId) { let attackRollData = this.getRollData(attackRollId); let defender = game.actors.get(actorId); defender.processNoDefense(attackRollData); } /* -------------------------------------------- */ static rerollHeroRemaining(userId, rollId) { if (game.user.isGM) { let user = game.users.get(userId) let character = user.character if (!character) { ui.notifications.warn(`No character linked to the player : reroll not allowed.`) return } console.log("Going to reroll", character, rollId) let rollData = this.getRollData(rollId) if (character.getLevelRemaining() > 0) { rollData.rerollHero = true this.rollPegasus(rollData) character.modifyHeroLevelRemaining(-1) } else { ui.notifications.warn(`No more Hero Level for ${character.name} ! Unable to reroll.`) } } } /* -------------------------------------------- */ static sendRerollHeroRemaining(userId, rollId) { game.socket.emit("system.fvtt-pegasus-rpg", { name: "msg_reroll_hero", data: { userId: userId, rollId: rollId } }) } /* -------------------------------------------- */ static targetToken(user, token, flag) { if (flag) { token.actor.checkIfPossible() } } /* -------------------------------------------- */ static async chatListeners(html) { html.on("click", '.chat-create-actor', event => { game.system.pegasus.creator.processChatEvent(event); }) html.on("click", '.view-item-from-chat', event => { game.system.pegasus.creator.openItemView(event) }) html.on("click", '.reroll-level-remaining', event => { let rollId = $(event.currentTarget).data("roll-id") if (game.user.isGM) { PegasusUtility.rerollHeroRemaining(game.user.id, rollId) } else { PegasusUtility.sendRerollHeroRemaining(game.user.id, rollId) } }) } /* -------------------------------------------- */ static async preloadHandlebarsTemplates() { const templatePaths = [ 'systems/fvtt-pegasus-rpg/templates/editor-notes-gm.html', 'systems/fvtt-pegasus-rpg/templates/partial-roll-select-effects.html', 'systems/fvtt-pegasus-rpg/templates/partial-options-statistics.html', 'systems/fvtt-pegasus-rpg/templates/partial-options-level.html', 'systems/fvtt-pegasus-rpg/templates/partial-options-range.html', 'systems/fvtt-pegasus-rpg/templates/partial-options-equipment-types.html', 'systems/fvtt-pegasus-rpg/templates/partial-equipment-effects.html', 'systems/fvtt-pegasus-rpg/templates/partial-actor-stat-block.html', 'systems/fvtt-pegasus-rpg/templates/partial-actor-status.html', 'systems/fvtt-pegasus-rpg/templates/partial-vehicle-stat-block.html', 'systems/fvtt-pegasus-rpg/templates/partial-vehicle-arc.html', 'systems/fvtt-pegasus-rpg/templates/partial-item-nav.html', 'systems/fvtt-pegasus-rpg/templates/partial-item-description.html', 'systems/fvtt-pegasus-rpg/templates/partial-actor-equipment.html', "systems/fvtt-pegasus-rpg/templates/partial-options-vehicle-speed.html" ] return loadTemplates(templatePaths); } /* -------------------------------------------- */ static async getEffectFromCompendium(effectName) { effectName = effectName.toLowerCase() let effect = game.items.contents.find(item => item.type == 'effect' && item.name.toLowerCase() == effectName) if (!effect) { let effects = await this.loadCompendium('fvtt-pegasus-rpg.effects', item => item.name.toLowerCase() == effectName) let objs = effects.map(i => i.toObject()) effect = objs[0] } else { effect = duplicate(effect); } console.log("Effect", effect) return effect } /* -------------------------------------------- */ static removeChatMessageId(messageId) { if (messageId) { game.messages.get(messageId)?.delete(); } } static findChatMessageId(current) { return PegasusUtility.getChatMessageId(PegasusUtility.findChatMessage(current)); } static getChatMessageId(node) { return node?.attributes.getNamedItem('data-message-id')?.value; } static findChatMessage(current) { return PegasusUtility.findNodeMatching(current, it => it.classList.contains('chat-message') && it.attributes.getNamedItem('data-message-id')); } static findNodeMatching(current, predicate) { if (current) { if (predicate(current)) { return current; } return PegasusUtility.findNodeMatching(current.parentElement, predicate); } return undefined; } /* -------------------------------------------- */ static getDiceValue(level = 0) { let diceString = this.diceList[level] if (!diceString) { return __name2DiceValue[level] } let diceTab = diceString.split(" ") let diceValue = 0 for (let dice of diceTab) { diceValue += __name2DiceValue[dice] } return diceValue } /* -------------------------------------------- */ static getLevelFromDice(dice) { return __dice2Level[dice] } /* -------------------------------------------- */ static getDiceFromLevel(level = 0) { level = Math.max(Number(level), 0) return this.diceList[level]; } /* -------------------------------------------- */ static getFoundryDiceFromLevel(level = 0) { level = Number(level) //console.log(this.diceFoundryList); return this.diceFoundryList[level]; } /* -------------------------------------------- */ static createDirectOptionList(min, max) { let options = {}; for (let i = min; i <= max; i++) { options[`${i}`] = `${i}`; } return options; } /* -------------------------------------------- */ static buildListOptions(min, max) { let options = "" for (let i = min; i <= max; i++) { options += `` } return options; } /* -------------------------------------------- */ static getTarget() { if (game.user.targets) { for (let target of game.user.targets) { return target } } return undefined; } /* -------------------------------------------- */ static computeDistance() { let mytarget = game.user.targets.first() console.log("target", mytarget, mytarget) let mytoken = _token if (mytarget) { let dist = canvas.grid.measureDistances( [{ ray: new Ray(mytoken.center, mytarget.center) }], { gridSpaces: true }); console.log("DIST", dist) } else { console.log("NO TARGET") } } /* -------------------------------------------- */ static getDefenseState(actorId) { return this.defenderStore[actorId]; } /* -------------------------------------------- */ static async updateDefenseState(defenderTokenId, rollId) { this.defenderStore[defenderTokenId] = rollId let defender = game.canvas.tokens.get(defenderTokenId).actor if (game.user.character && game.user.character.id == defender.id) { let chatData = { user: game.user.id, alias: defender.name, rollMode: game.settings.get("core", "rollMode"), whisper: [game.user.id].concat(ChatMessage.getWhisperRecipients('GM')), content: `
${defender.name} is under attack. He must roll a skill/weapon/technique to defend himself or suffer damages (button below). u.id); if (chatData.rollMode === "blindroll") chatData["blind"] = true; else if (chatData.rollMode === "selfroll") chatData["whisper"] = [game.user]; if (forceWhisper) { // Final force ! chatData["speaker"] = ChatMessage.getSpeaker(); chatData["whisper"] = ChatMessage.getWhisperRecipients(forceWhisper); } return chatData; } /* -------------------------------------------- */ static async showDiceSoNice(roll, rollMode) { if (game.modules.get("dice-so-nice")?.active) { if (game.dice3d) { let whisper = null; let blind = false; rollMode = rollMode ?? game.settings.get("core", "rollMode"); switch (rollMode) { case "blindroll": //GM only blind = true; case "gmroll": //GM + rolling player whisper = this.getUsers(user => user.isGM); break; case "roll": //everybody whisper = this.getUsers(user => user.active); break; case "selfroll": whisper = [game.user.id]; break; } await game.dice3d.showForRoll(roll, game.user, true, whisper, blind); } } } /* -------------------------------------------- */ static removeUsedPerkEffects(rollData) { // De-actived used effects from perks let toRem = [] for (let effect of rollData.effectsList) { if (effect.effect.system.perkId && effect.effect.system.isUsed) { toRem.push(effect.effect._id) } } if (toRem.length > 0) { let actor = game.actors.get(rollData.actorId) actor.deleteEmbeddedDocuments('Item', toRem) } } /* -------------------------------------------- */ static removeOneUseEffects(rollData) { // De-actived used effects from perks let toRem = [] for (let effect of rollData.effectsList) { if (effect?.effect?.system.isUsed && effect.effect.system.oneuse) { effect.defenderTokenId = rollData.defenderTokenId if (effect.foreign) { if (game.user.isGM) { this.removeForeignEffect(effect) } else { game.socket.emit("system.fvtt-pegasus-rpg", { msg: "msg_gm_remove_effect", data: effect }) } } else { toRem.push(effect.effect._id) ChatMessage.create({ content: `One used effect ${effect.effect.name} has been auto-deleted.` }) } } } if (toRem.length > 0) { //console.log("Going to remove one use effects", toRem) let actor = game.actors.get(rollData.actorId) actor.deleteEmbeddedDocuments('Item', toRem) } } /* -------------------------------------------- */ static async momentumReroll(actorId) { let actor = game.actors.get(actorId) let rollData = actor.lastRoll if (rollData) { rollData.rerollMomentum = true PegasusUtility.rollPegasus(rollData) this.actor.lastRoll = undefined } else { ui.notifications.warn("No last roll registered....") } } /* -------------------------------------------- */ static async showMomentumDialog(actorId) { let d = new Dialog({ title: "Momentum reroll", 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) if (rollData.tokenId) { let token = canvas.tokens.placeables.find(t => t.id == rollData.tokenId) if (token) { actor = token.actor } } 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 }) rollData.roll = myRoll } if (rollData.hindranceDices > 0) { rollData.hindranceRoll = new Roll(rollData.hindranceDices + "dh").roll({ async: false }) this.showDiceSoNice(rollData.hindranceRoll, game.settings.get("core", "rollMode")) for (let res of rollData.hindranceRoll.terms[0].results) { if (res.result == 6) { rollData.hindranceFailure = true rollData.img = `systems/fvtt-pegasus-rpg/images/dice/hindrance-dice.png` } } } await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode")) // Final score and keep data rollData.finalScore = (rollData.hindranceFailure) ? 0 : 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) await combat.setTic(rollData.combatantId, rollData) 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 + "
" + chatOptions.content; console.log("blindMessageToGM", chatGM); game.socket.emit("system.fvtt-pegasus-rpg", { msg: "msg_gm_chat_message", data: chatGM }); } /* -------------------------------------------- */ static async searchItem(dataItem) { let item if (dataItem.pack) { let id = dataItem.id || dataItem._id let items = await this.loadCompendium(dataItem.pack, item => item.id == id) //console.log(">>>>>> PACK", items) item = items[0] || undefined //item = await fromUuid(dataItem.pack + "." + id) } else { item = game.items.get(dataItem.id) } return item } /* -------------------------------------------- */ static split3Columns(data) { let array = [[], [], []]; if (data == undefined) return array; let col = 0; for (let key in data) { let keyword = data[key]; keyword.key = key; // Self-reference array[col].push(keyword); col++; if (col == 3) col = 0; } return array; } /* -------------------------------------------- */ static createChatMessage(name, rollMode, chatOptions) { switch (rollMode) { case "blindroll": // GM only if (!game.user.isGM) { this.blindMessageToGM(chatOptions); chatOptions.whisper = [game.user.id]; chatOptions.content = "Message only to the GM"; } else { chatOptions.whisper = this.getUsers(user => user.isGM); } break; default: chatOptions.whisper = this.getWhisperRecipients(rollMode, name); break; } chatOptions.alias = chatOptions.alias || name; ChatMessage.create(chatOptions); } /* -------------------------------------------- */ static getBasicRollData(isInit) { let rollData = { rollId: randomID(16), rollMode: game.settings.get("core", "rollMode"), bonusDicesLevel: 0, statLevelBonus: 0, damageLevelBonus: 0, specLevelBonus: 0, hindranceLevelBonus: 0, hindranceDicesLevel: 0, modifiers: "none", otherDicesLevel: 0, statDicesLevel: 0, specDicesLevel: 0, effectsList: [], armorsList: [], weaponsList: [], vehicleWeapons: [], equipmentsList: [], vehicleShieldList: [], optionsDiceList: PegasusUtility.getOptionsDiceList() } if (!isInit) { // For init, do not display target hindrances PegasusUtility.updateWithTarget(rollData) } return rollData } /* -------------------------------------------- */ static updateWithTarget(rollData) { let target = PegasusUtility.getTarget() if (target) { let defenderActor = target.actor rollData.defenderTokenId = target.id rollData.defenderSize = 0 if (defenderActor.type == "character") { rollData.defenderSize = Number(defenderActor.system.biodata.sizenum) + Number(defenderActor.system.biodata.sizebonus) } else if (defenderActor.type == "vehicle") { rollData.defenderSize = Number(defenderActor.system.statistics.hr.size) } //rollData.attackerId = this.id console.log("Target/DEFENDER", defenderActor) //defenderActor.addHindrancesList(rollData.effectsList) /* No more used */ } } /* -------------------------------------------- */ static createChatWithRollMode(name, chatOptions) { this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions); } /* -------------------------------------------- */ static async confirmDelete(actorSheet, li) { let itemId = li.data("item-id"); let msgTxt = "

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.id] || { token: friend, 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.id] = existing } } } for (let id in toApply) { let applyDef = toApply[id] let hasBonus = applyDef.token.actor.hasTacticianBonus() if (applyDef.add) { if (!hasBonus) { await applyDef.token.actor.addTacticianEffect(applyDef.names.toString(), applyDef.level) } else if (applyDef.level != hasBonus.system.effectlevel) { await applyDef.token.actor.removeTacticianEffect() await applyDef.token.actor.addTacticianEffect(applyDef.names.toString(), applyDef.level) } } else if (hasBonus) { await applyDef.token.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()) { await 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.id] || { token: friend, 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.id] = existing } } } for (let id in toApply) { let applyDef = toApply[id] let hasBonus = applyDef.token.actor.hasEnhancerBonus() if (applyDef.add) { if (!hasBonus) { await applyDef.token.actor.addEnhancerEffect(applyDef.names.toString(), applyDef.level) } else if (applyDef.level != hasBonus.system.effectlevel) { await applyDef.token.actor.removeEnhancerEffect() await applyDef.token.actor.addEnhancerEffect(applyDef.names.toString(), applyDef.level) } } else if (hasBonus) { await applyDef.token.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()) { await 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() let ennemies = [] if (token.document.disposition == -1) { ennemies = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && (newToken.document.disposition == 1 || newToken.document.disposition == 0)) } if (token.document.disposition == 1) { ennemies = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && (newToken.document.disposition == -1 || newToken.document.disposition == 0)) } if (token.document.disposition == 0) { ennemies = canvas.tokens.placeables.filter(newToken => newToken.actor.type == "character" && !newToken.document.hidden && (newToken.document.disposition == -1 || newToken.document.disposition == 1)) } console.log("Ennemies for token", token.actor.name, ennemies) for (let ennemy of ennemies) { if (ennemy.actor.id != token.actor.id) { //console.log("Adding ennemy", ennemy.id) let existing = toApply[ennemy.id] || { token: ennemy, 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.id] = existing } } } //console.log("To apply stuff : ", toApply) for (let id in toApply) { let applyDef = toApply[id] let hasHindrance = applyDef.token.actor.hasAgitatorHindrance() if (applyDef.add) { if (!hasHindrance) { await applyDef.token.actor.addAgitatorHindrance(applyDef.names.toString(), applyDef.level) } else if (applyDef.level != hasHindrance.system.effectlevel) { await applyDef.token.actor.removeAgitatorHindrance() await applyDef.token.actor.addAgitatorHindrance(applyDef.names.toString(), applyDef.level) } } else if (hasHindrance) { await applyDef.token.actor.removeAgitatorHindrance() } } // Delete all effects if no more agitators (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()) { await 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") /*NO MORE USED : await this.processTactician()*/ await this.processEnhancer() /*NO MORE USED : await this.processAgitator()*/ } /* -------------------------------------------- */ static async refreshSightForEffect() { setTimeout(500, this.processRoleEffects()) } }