/* -------------------------------------------- */ import { WastelandCombat } from "./wasteland-combat.js"; import { WastelandCommands } from "./wasteland-commands.js"; /* -------------------------------------------- */ const __contrecouptCharme = { 1 : {name: "Effet chromatique", description: "le corps du kobold prend des teintes aussi étranges que voyantes. L'effet s’estompe progressivement et 24 heures plus tard, le kobold retrouve ses couleurs d’origine." }, 3 : {name: "Enivrement Kobold", description: "très excité par son premier tour, le kobold doit immédiatement faire un autre tour, pour lequel il emploiera un dé plus gros." }, 5 : {name: "Mutisme superstitieux", description: "le kobold ne doit plus parler» pendant les prochaines 24 heures. S'il le fait malgré tout, les effets de son tour s’arrêtent." }, 7 : {name: "Agité!", description: "le kobold ne tient plus en place. Il ne peut se reposer pendant les prochaines 12 heures. Dès. que 12 heures se sont écoulées, il s'effondre comme une masse et dort 12 heures d'affilée d’un sommeil enchanté dont rien ne pourra le réveiller." }, 9 : {name: "Somnolence", description: "le kobold devient somnolent. Il n’arrive pas à se concentrer même sur une tâche simple, bäille sans arrêt, traîne les pieds et n’agit plus que de mauvaise grâce. Cela dure jusqu’à ce qu'il ait dormi au moins 12 heures." }, 11 : {name: "Manie incontrôlable", description: "le kobold est pris d’une manie incontrôlable. Dès qu'il voit un chapeau rouge, il doit suivre son porteur. Il ne mangera que si son voisin de gauche mange aussi, etc. Cela dure pendant une jour- née puis l’effet s’inverse pendant une heure : il ne suivra jamais un chapeau rouge, ne mangera jamais si son voi- sin de gauche mange, etc. Le contrecoup prend alors fin." }, 13 : {name: "Malédiction des Ternes", description: "le kobold perd cette qualité mystérieuse qui fait que les kobolds sont des kobolds et devient tout. Terne. Il perd 1d20 point(s) de Bonne Aventure (s’il doit en perdre plus qu'il n’en a, il tombe simplement à 0). Ces points perdus pourront cependant être regagnés normalement." }, 15 : {name: "La petite Mort", description: "le kobold s'endort pour 1420 heures. Rien ni personne ne pourra le tirer de ce sommeil enchanté avant que ce contrecoup ne prenne fin." }, 17 : {name: "Angoisse cauchemardesque", description: "le kobold a une brève vision de pure horreur. Il perd 1420 points de Psyché {s'il doit en perdre plus qu'il n’en a, il tombe à 0)." }, 19 : {name: "Anémie Kobold", description: "le kobold se met à saigner du nez, des oreilles et même d’autres endroits. Il perd 1420 point(s) de Santé." } } /* -------------------------------------------- */ export class WastelandUtility { /* -------------------------------------------- */ static async init() { Hooks.on('renderChatLog', (log, html, data) => WastelandUtility.chatListeners(html)) Hooks.on("getChatLogEntryContext", (html, options) => WastelandUtility.chatRollMenu(html, options)) Hooks.on("getCombatTrackerEntryContext", (html, options) => { WastelandUtility.pushInitiativeOptions(html, options); }) Hooks.on("dropCanvasData", (canvas, data) => { WastelandUtility.dropItemOnToken(canvas, data) }); this.rollDataStore = {} this.defenderStore = {} WastelandCommands.init(); Handlebars.registerHelper('count', function (list) { return list.length; }) 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); }) } /* -------------------------------------------- */ static getActorFromRollData(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 } } return actor } /* -------------------------------------------- */ static getModificateurOptions() { let opt = [] for (let i = -15; i <= 15; i++) { opt.push(``) } return opt.concat("\n") } /* -------------------------------------------- */ static sortArrayObjectsByName(myArray) { myArray.sort((a, b) => { return a.name.localeCompare(b.name); }) } /* -------------------------------------------- */ static getPointAmeOptions() { let opt = [] for (let i = 1; i <= 20; i++) { opt.push(``) } return opt.concat("\n") } /* -------------------------------------------- */ static getAttributs() { return { adr: "Adresse", pui: "Puissance", cla: "Clairvoyance", pre: "Présence", tre: "Trempe" } } /* -------------------------------------------- */ static pushInitiativeOptions(html, options) { } /* -------------------------------------------- */ static getSkills() { return this.skills } /* -------------------------------------------- */ static async ready() { const skills = await WastelandUtility.loadCompendium("fvtt-wasteland.skills") this.skills = skills.map(i => i.toObject()) } /* -------------------------------------------- */ static async loadCompendiumData(compendium) { const pack = game.packs.get(compendium); return await pack?.getDocuments() ?? []; } /* -------------------------------------------- */ static async loadCompendium(compendium, filter = item => true) { let compendiumData = await WastelandUtility.loadCompendiumData(compendium); return compendiumData.filter(filter); } /* -------------------------------------------- */ static getOptionsStatusList() { return this.optionsStatusList; } /* -------------------------------------------- */ static async chatListeners(html) { html.on("click", '.predilection-reroll', async event => { let predIdx = $(event.currentTarget).data("predilection-index") let messageId = WastelandUtility.findChatMessageId(event.currentTarget) let message = game.messages.get(messageId) let rollData = message.getFlag("world", "wasteland-roll") let actor = WastelandUtility.getActorFromRollData(rollData) await actor.setPredilectionUsed(rollData.competence._id, predIdx) rollData.competence = duplicate(actor.getCompetence(rollData.competence._id)) await WastelandUtility.rollWasteland(rollData) }) } /* -------------------------------------------- */ static async preloadHandlebarsTemplates() { const templatePaths = [ 'systems/fvtt-wasteland/templates/editor-notes-gm.html', 'systems/fvtt-wasteland/templates/partial-item-description.html', 'systems/fvtt-wasteland/templates/partial-list-niveau.html', 'systems/fvtt-wasteland/templates/partial-list-niveau-creature.html' ] return loadTemplates(templatePaths); } /* -------------------------------------------- */ static removeChatMessageId(messageId) { if (messageId) { game.messages.get(messageId)?.delete(); } } static findChatMessageId(current) { return WastelandUtility.getChatMessageId(WastelandUtility.findChatMessage(current)); } static getChatMessageId(node) { return node?.attributes.getNamedItem('data-message-id')?.value; } static findChatMessage(current) { return WastelandUtility.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 WastelandUtility.findNodeMatching(current.parentElement, predicate); } return undefined; } /* -------------------------------------------- */ 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 && game.user.targets.size == 1) { for (let target of game.user.targets) { return target; } } return undefined; } /* -------------------------------------------- */ static updateRollData(rollData) { let id = rollData.rollId; let oldRollData = this.rollDataStore[id] || {}; let newRollData = mergeObject(oldRollData, rollData); this.rollDataStore[id] = newRollData; } /* -------------------------------------------- */ static saveRollData(rollData) { game.socket.emit("system.fvtt-wasteland", { name: "msg_update_roll", data: rollData }); // Notify all other clients of the roll this.updateRollData(rollData); } /* -------------------------------------------- */ static getRollData(id) { return this.rollDataStore[id]; } /* -------------------------------------------- */ static onSocketMesssage(msg) { if (msg.name == "msg_update_defense_state") { this.updateDefenseState(msg.data.defenderId, msg.data.rollId); } if (msg.name == "msg_update_roll") { this.updateRollData(msg.data); } } /* -------------------------------------------- */ static chatDataSetup(content, modeOverride, isRoll = false, forceWhisper) { let chatData = { user: game.user.id, rollMode: modeOverride || game.settings.get("core", "rollMode"), content: content }; if (["gmroll", "blindroll"].includes(chatData.rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM").map(u => 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 computeResult(rollData, actor) { if (rollData.charme) { let resultIndex = false let resTab = duplicate(rollData.charme.system.resultats) for(let id in resTab) { let res = resTab[id] if (!resultIndex && rollData.finalResult >= res.value) { resultIndex = id; } } if (resultIndex) { rollData.charmeDuree = rollData.charme.system.resultats[resultIndex].description } let effectRoll = new Roll(rollData.charmeDice).roll({ async: false }) if (rollData.charme.system.charmetype == "tour") { rollData.contrecoupResult = effectRoll.total if (rollData.contrecoupResult % 2 == 1) { rollData.contrecoup = __contrecouptCharme[rollData.contrecoupResult] } } if (rollData.charme.system.charmetype == "charme") { rollData.charmeSante = effectRoll.total actor.incDecSante(rollData.charmeSante) } } else { if (rollData.mainDice == "1d20") { let diceValue = rollData.roll.terms[0].results[0].result diceValue *= (rollData.doubleD20) ? 2 : 1 //console.log("PAIR/IMP", diceValue) if (diceValue % 2 == 1) { //console.log("PAIR/IMP2", diceValue) rollData.finalResult -= rollData.roll.terms[0].results[0].result // Substract value if (diceValue == 1 || diceValue == 11) { rollData.isDramatique = true rollData.isSuccess = false } } } //console.log("Result : ", rollData) if (rollData.difficulte > 0 && !rollData.isDramatique) { rollData.isSuccess = (rollData.finalResult >= rollData.difficulte) rollData.isHeroique = ((rollData.finalResult - rollData.difficulte) >= 10) rollData.isDramatique = ((rollData.finalResult - rollData.difficulte) <= -10) } } } /* -------------------------------------------- */ static async rollWasteland(rollData) { let actor = WastelandUtility.getActorFromRollData(rollData) if (rollData.attrKey == "tochoose") { // No attr selected, force address rollData.attrKey = "adr" } if (!rollData.attr) { rollData.actionImg = "systems/fvtt-wasteland/assets/icons/" + actor.system.attributs[rollData.attrKey].labelnorm + ".webp" rollData.attr = duplicate(actor.system.attributs[rollData.attrKey]) } if (rollData.charme) { rollData.diceFormula = rollData.charmeDice } else { rollData.diceFormula = rollData.mainDice if (rollData.doubleD20) { // Multiply result ! rollData.diceFormula += "*2" if (!rollData.isReroll) { actor.changeEclat(-1) } } } //console.log("BEFORE COMP", rollData) if (rollData.competence) { rollData.predilections = duplicate(rollData.competence.system.predilections.filter(pred => !pred.used) || []) let compmod = (rollData.competence.system.niveau == 0) ? -3 : 0 rollData.diceFormula += `+${rollData.attr.value}+${rollData.competence.system.niveau}+${rollData.modificateur}+${compmod}` } else { rollData.diceFormula += `+${rollData.attr.value}*2+${rollData.modificateur}` } if (rollData.arme && rollData.arme.type == "arme") { rollData.diceFormula += `+${rollData.arme.system.bonusmaniementoff}` } let myRoll = new Roll(rollData.diceFormula).roll({ async: false }) await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode")) rollData.roll = duplicate(myRoll) rollData.diceResult = myRoll.terms[0].results[0].result console.log(">>>> ", myRoll) rollData.finalResult = myRoll.total this.computeResult(rollData, actor) this.createChatWithRollMode(rollData.alias, { content: await renderTemplate(`systems/fvtt-wasteland/templates/chat-generic-result.html`, rollData) }, rollData) } /* -------------------------------------------- */ static async bonusRollWasteland(rollData) { rollData.bonusFormula = rollData.addedBonus let bonusRoll = new Roll(rollData.bonusFormula).roll({ async: false }) await this.showDiceSoNice(bonusRoll, game.settings.get("core", "rollMode")); rollData.bonusRoll = duplicate(bonusRoll) rollData.finalResult += rollData.bonusRoll.total this.computeResult(rollData) this.createChatWithRollMode(rollData.alias, { content: await renderTemplate(`systems/fvtt-wasteland/templates/chat-generic-result.html`, rollData) }, rollData) } /* -------------------------------------------- */ static getUsers(filter) { return game.users.filter(filter).map(user => user.data._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-weapons-of-the-gods", { msg: "msg_gm_chat_message", data: chatGM }); } /* -------------------------------------------- */ static async searchItem(dataItem) { let item; if (dataItem.pack) { item = await fromUuid("Compendium." + dataItem.pack + "." + dataItem.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 async createChatMessage(name, rollMode, chatOptions, rollData = undefined) { 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 let msg = await ChatMessage.create(chatOptions) console.log("=======>", rollData) msg.setFlag("world", "wasteland-roll", rollData) } /* -------------------------------------------- */ static getBasicRollData() { let rollData = { rollId: randomID(16), rollMode: game.settings.get("core", "rollMode"), modificateursOptions: this.getModificateurOptions(), pointAmeOptions: this.getPointAmeOptions(), difficulte: 0, modificateur: 0, } WastelandUtility.updateWithTarget(rollData) return rollData } /* -------------------------------------------- */ static updateWithTarget(rollData) { let target = WastelandUtility.getTarget() if (target) { rollData.defenderTokenId = target.id let defender = game.canvas.tokens.get(rollData.defenderTokenId).actor rollData.armeDefense = defender.getBestDefenseValue() if (rollData.armeDefense) { rollData.difficulte = rollData.armeDefense.system.totalDefensif } else { ui.notifications.warn("Aucune arme de défense équipée, difficulté manuelle à positionner.") } } } /* -------------------------------------------- */ static createChatWithRollMode(name, chatOptions, rollData = undefined) { this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions, rollData) } /* -------------------------------------------- */ static applyBonneAventureRoll(li, changed, addedBonus) { let msgId = li.data("message-id") let msg = game.messages.get(msgId) if (msg) { let rollData = msg.getFlag("world", "wasteland-roll") let actor = WastelandUtility.getActorFromRollData(rollData) actor.changeBonneAventure(changed) rollData.isReroll = true rollData.textBonus = "Bonus de Points d'Aventure" if (addedBonus == "reroll") { WastelandUtility.rollWasteland(rollData) } else { rollData.addedBonus = addedBonus WastelandUtility.bonusRollWasteland(rollData) } } } /* -------------------------------------------- */ static applyEclatRoll(li, changed, addedBonus) { let msgId = li.data("message-id") let msg = game.messages.get(msgId) if (msg) { let rollData = msg.getFlag("world", "wasteland-roll") let actor = WastelandUtility.getActorFromRollData(rollData) actor.changeEclat(changed) rollData.isReroll = true rollData.textBonus = "Bonus d'Eclat" rollData.addedBonus = addedBonus WastelandUtility.bonusRollWasteland(rollData) } } /* -------------------------------------------- */ static chatRollMenu(html, options) { let canApply = li => canvas.tokens.controlled.length && li.find(".wasteland-roll").length let hasBA = function (li) { let message = game.messages.get(li.attr("data-message-id")) let rollData = message.getFlag("world", "wasteland-roll") if (rollData?.actorId) { let actor = game.actors.get(rollData.actorId) return actor.getBonneAventure() > 0 } return false } let hasBA2 = function (li) { let message = game.messages.get(li.attr("data-message-id")) let rollData = message.getFlag("world", "wasteland-roll") if (rollData?.actorId) { let actor = game.actors.get(rollData.actorId) return actor.getBonneAventure() >= 2 } return false } let hasBA3 = function (li) { let message = game.messages.get(li.attr("data-message-id")) let rollData = message.getFlag("world", "wasteland-roll") if (rollData?.actorId) { let actor = game.actors.get(rollData.actorId) return actor.getBonneAventure() >= 3 } return false } let hasPE = function (li) { let message = game.messages.get(li.attr("data-message-id")) let rollData = message.getFlag("world", "wasteland-roll") if (rollData?.actorId) { let actor = game.actors.get(rollData.actorId) return actor.getEclat() >= 1 } return false } let hasPredilection = function (li) { let message = game.messages.get(li.attr("data-message-id")) let rollData = message.getFlag("world", "wasteland-roll") if (rollData.competence) { let nbPred = rollData.competence.data.predilections.filter(pred => !pred.used).length return (!rollData.isReroll && rollData.competence && nbPred > 0) } return false } let canCompetenceDouble = function (li) { let message = game.messages.get(li.attr("data-message-id")) let rollData = message.getFlag("world", "wasteland-roll") if (rollData.competence) { return rollData.competence.data.doublebonus } return false } options.push( { name: "Ajouter +3 (1 point de Bonne Aventure)", icon: "", condition: canApply && hasBA, callback: li => WastelandUtility.applyBonneAventureRoll(li, -1, "+3") } ) options.push( { name: "Gain de 1 Point de Santé / 24 heure (1 point de Bonne Aventure)", icon: "", condition: canApply && hasBA, callback: li => WastelandUtility.incDecSante(1) } ) options.push( { name: "Gain de 2 Points de Santé / 24 heure (2 points de Bonne Aventure)", icon: "", condition: canApply && hasBA2, callback: li => WastelandUtility.incDecSante(2) } ) options.push( { name: "Relancer le jet (3 points de Bonne Aventure)", icon: "", condition: canApply && hasBA3, callback: li => WastelandUtility.applyBonneAventureRoll(li, -3, "reroll") } ) options.push( { name: "Bénéficier d'1 action supplémentaire (3 points de Bonne Aventure)", icon: "", condition: canApply && hasBA3, callback: li => WastelandUtility.applyBonneAventureRoll(li, -3, "newaction") } ) options.push( { name: "Ajouter +10 (1 Point d'Eclat)", icon: "", condition: canApply && hasPE, callback: li => WastelandUtility.applyEclatRoll(li, -1, "+10") } ) options.push( { name: "Double le résultat du d20 (1 Point d'Eclat)", icon: "", condition: canApply && hasPE, callback: li => WastelandUtility.applyEclatRoll(li, -1, "double20") } ) options.push( { name: "Annuler une blessure (1 Point d'Eclat)", icon: "", condition: canApply && hasPE, callback: li => WastelandUtility.cancelBlessure(li) } ) options.push( { name: "Recharger ses points de BA (1 Point d'Eclat)", icon: "", condition: canApply && hasPE, callback: li => WastelandUtility.reloadBA(li) } ) return options } /* -------------------------------------------- */ 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); } }