/* -------------------------------------------- */ import { SoSCombat } from "./sos-combat.js"; import { SoSDialogCombatActions } from "./sos-dialog-combat-actions.js"; /* -------------------------------------------- */ const severity2malus = { "none": 0, "light": -1, "moderate": -2, "severe": -3, "critical": -4 }; /* -------------------------------------------- */ const severity2bonus = { "none": 0, "light": 1, "moderate": 2, "severe": 3, "critical": 4 }; /* -------------------------------------------- */ export class SoSUtility { /* -------------------------------------------- */ static async preloadHandlebarsTemplates() { const templatePaths = [ 'systems/foundryvtt-shadows-over-sol/templates/actor-sheet.html', 'systems/foundryvtt-shadows-over-sol/templates/editor-notes-gm.html', 'systems/foundryvtt-shadows-over-sol/templates/stat-option-list.html', 'systems/foundryvtt-shadows-over-sol/templates/stat-name-list.html', 'systems/foundryvtt-shadows-over-sol/templates/item-sheet.html', 'systems/foundryvtt-shadows-over-sol/templates/item-geneline-sheet.html', 'systems/foundryvtt-shadows-over-sol/templates/item-subculture-sheet.html', 'systems/foundryvtt-shadows-over-sol/templates/item-weapon-sheet.html', 'systems/foundryvtt-shadows-over-sol/templates/item-commongear-sheet.html', 'systems/foundryvtt-shadows-over-sol/templates/dialog-flip.html' ] return loadTemplates(templatePaths); } /* -------------------------------------------- */ static ready() { Handlebars.registerHelper('select', function (selected, options) { const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected)); const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']'); const html = options.fn(this); return html.replace(rgx, "$& selected"); }); } /* -------------------------------------------- */ static fillRange(start, end) { return Array(end - start + 1).fill().map((item, index) => start + index); } /* -------------------------------------------- */ static onSocketMesssage(msg) { if (!game.user.isGM) return; // Only GM if (msg.name == 'msg_declare_actions') { let combat = game.combats.get(msg.data.combatId); // Get the associated combat combat.setupActorActions(msg.data); } else if (msg.name == 'msg_close_action') { game.combat.closeAction(msg.data.uniqId); } else if (msg.name == 'msg_request_defense') { SoSUtility.applyDamage(msg.data); } else if (msg.name == 'msg_reaction_cover') { SoSUtility.reactionCover(msg.data.uniqId); } else if (msg.name == 'msg_reaction_melee') { SoSUtility.reactionMelee(msg.data.uniqId); } else if (msg.name == 'msg_reaction_hit') { SoSUtility.reactionHit(msg.data.uniqId); } } /* -------------------------------------------- */ static async loadCompendiumData(compendium) { const pack = game.packs.get(compendium); return await pack?.getDocuments() ?? []; } /* -------------------------------------------- */ static async loadCompendium(compendium, filter = item => true) { let compendiumData = await SoSUtility.loadCompendiumData(compendium); return compendiumData.filter(filter); } /* -------------------------------------------- */ static async loadCompendiumNames(compendium) { const pack = game.packs.get(compendium); let competences; await pack.getIndex().then(index => competences = index); return competences; } /* -------------------------------------------- */ static updateCombat(combat, round, diff, id) { combat.requestActions(); } /* -------------------------------------------- */ static async openDeclareActions(event) { event.preventDefault(); let round = event.currentTarget.attributes['data-round'].value; let combatantId = event.currentTarget.attributes['data-combatant-id'].value; let combatId = event.currentTarget.attributes['data-combat-id'].value; let uniqId = event.currentTarget.attributes['data-uniq-id'].value; let d = await SoSDialogCombatActions.create(combatId, combatantId, round, uniqId); d.render(true); } /* -------------------------------------------- */ static getConsequenceMalus(severity) { return severity2malus[severity] ?? 0; } /* -------------------------------------------- */ static getConsequenceBonus(severity) { return severity2bonus[severity] ?? 0; } /* -------------------------------------------- */ static computeEncumbrance(items) { let trappings = items.filter(item => item.type == 'gear' || item.type == 'armor' || item.type == 'weapon'); let sumEnc = 0; for (let object of trappings) { if ((!object.system.worn) && (!object.system.neg) && (!object.system.software) && (!object.system.implant) && (!object.system.containerid || object.system.containerid == "")) { sumEnc += (object.big > 0) ? object.big : 1; } } return sumEnc; } /* -------------------------------------------- */ static closeAction(event) { let uniqId = event.currentTarget.attributes['data-uniq-id'].value; if (game.user.isGM) { game.combat.closeAction(uniqId); } else { game.socket.emit("system.foundryvtt-shadows-over-sol", { name: "msg_close_action", data: { uniqId: uniqId } }); } } /* -------------------------------------------- */ static async registerChatCallbacks(html) { html.on("click", '#button-declare-actions', event => { SoSUtility.openDeclareActions(event); }); html.on("click", '#button-end-action', event => { SoSUtility.closeAction(event); }); html.on("click", '#button-reaction-cover', event => { let uniqId = event.currentTarget.attributes['data-uniq-id'].value; if (game.user.isGM) { SoSUtility.reactionCover(uniqId); } else { game.socket.emit("system.foundryvtt-shadows-over-sol", { name: "msg_reaction_cover", data: { uniqId: uniqId } }); } }); html.on("click", '#button-reaction-melee', event => { let uniqId = event.currentTarget.attributes['data-uniq-id'].value; if (game.user.isGM) { SoSUtility.reactionMelee(uniqId); } else { game.socket.emit("system.foundryvtt-shadows-over-sol", { name: "msg_reaction_melee", data: { uniqId: uniqId } }); } }); html.on("click", '#button-reaction-hit', event => { let uniqId = event.currentTarget.attributes['data-uniq-id'].value; if (game.user.isGM) { SoSUtility.reactionHit(uniqId); } else { game.socket.emit("system.foundryvtt-shadows-over-sol", { name: "msg_reaction_hit", data: { uniqId: uniqId } }); } }); } /* -------------------------------------------- */ static getTarget() { if (game.user.targets && game.user.targets.size == 1) { for (let target of game.user.targets) { return target; } } return undefined; } /* -------------------------------------------- */ static increaseConsequenceSeverity(severity) { if (severity == 'none') return 'light'; if (severity == 'light') return 'moderate'; if (severity == 'moderate') return 'severe'; if (severity == 'severe') return 'critical'; return 'critical'; } /* -------------------------------------------- */ static getConsequenceSeverityLevel(severity) { if (severity == 'none') return 0; if (severity == 'light') return 1; if (severity == 'moderate') return 2; if (severity == 'severe') return 3; if (severity == 'critical') return 4; return 0; } /* -------------------------------------------- */ static increaseSeverity(severity) { if (severity == 'L') return 'M'; if (severity == 'M') return 'S'; if (severity == 'S') return 'C'; if (severity == 'C') return 'F'; } /* -------------------------------------------- */ static decreaseSeverity(severity) { if (severity == 'C') return 'S'; if (severity == 'S') return 'M'; if (severity == 'M') return 'L'; if (severity == 'L') return 'N'; } /* -------------------------------------------- */ static getSeverityLevel(severity) { if (severity == 'C') return 4; if (severity == 'S') return 3; if (severity == 'M') return 2; if (severity == 'L') return 1; return 0; } /* -------------------------------------------- */ static async confirmDelete(actorSheet, li) { let itemId = li.data("item-id"); let objet = actorSheet.actor.items.find(item => item._id == itemId); let msgTxt = "
Are you sure to delete this item ?"; let buttons = { delete: { icon: '', label: "Yes, delete it", callback: () => { console.log("Delete : ", itemId); actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]); li.slideUp(200, () => actorSheet.render(false)); } }, cancel: { icon: '', label: "Cancel" } } msgTxt += "
"; let d = new Dialog({ title: "Confirm deletion", content: msgTxt, buttons: buttons, default: "cancel" }); d.render(true); } /* -------------------------------------------- */ static async applyDamage(flipData) { if (!this.registry) this.registry = {}; if (flipData.isReaction) { // Check again resut in case of reaction ! flipData.magnitude = flipData.finalScore - flipData.tn; // Update magnitude if (flipData.magnitude < 0) { let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-reaction-result.html', flipData); ChatMessage.create({ content: html }); return; } } // DR management let armor = flipData.target.actor.system.items.find(item => item.type == 'armor' && item.system.worn); flipData.armorDR = armor ? armor.system.dr : 0; flipData.armorGel = armor ? armor.system.gel : 0; flipData.armorReflect = armor ? armor.system.reflect : 0; let dr = flipData.target.actor.system.scores.dr.value + flipData.armorDR; if (flipData.weapon.system.category == 'ballistic') { dr += flipData.armorGel; } if (flipData.weapon.system.category == 'laser') { dr += flipData.armorReflect; } let shock = flipData.target.actor.system.scores.shock.value || 1; let defenseCritical = flipData.target.actor.system.scores.defense.critical; flipData.damageStatus = 'apply_damage'; flipData.targetShock = shock; flipData.targetDR = dr; flipData.targetCritical = defenseCritical; // DR management if (flipData.damageValue < dr) { if (flipData.damageValue < dr / 2) { flipData.damageStatus = "no_damage"; flipData.damageReason = "Damage are lesser than DR/2"; } else { flipData.damageSeverity = this.decreaseSeverity(flipData.damageSeverity); if (flipData.damageSeverity == 'N') { flipData.damageStatus = "no_damage"; flipData.damageReason = "Severity decreased to nothing"; } } } // Shock management flipData.woundsList = []; flipData.nbStun = 0; if (flipData.weapon.stun) { // Stun weapon case if (flipData.damageValue >= shock) { flipData.nbStun = Math.floor(flipData.damageValue / shock); } } else { if (flipData.damageValue >= shock) { let incSeverity = Math.floor(flipData.damageValue / shock); for (let i = 0; i < incSeverity; i++) { if (flipData.damageSeverity == 'C') { flipData.woundsList.push(flipData.damageSeverity); flipData.damageSeverity = 'L'; } else { flipData.nbStun++; flipData.damageSeverity = this.increaseSeverity(flipData.damageSeverity); flipData.damageReason = "Severity increased"; } } } } flipData.woundsList.push(flipData.damageSeverity); flipData.nbWounds = flipData.woundsList.length; // Critical management flipData.isCritical = (flipData.cardTotal >= defenseCritical); let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-damage-target.html', flipData); ChatMessage.create({ content: html }); // Is target able to dodge ?? let defender = game.actors.get(flipData.target.actor._id); flipData.coverConsequence = defender.items.find(item => item.type == 'consequence' && item.name == 'Cover'); flipData.APavailable = game.combat.getAPFromActor(defender._id); console.log("FLIPDATE : ", flipData); if (!flipData.isReaction && flipData.APavailable > 0) { if ((flipData.weapon.system.category == 'melee') || ((flipData.weapon.system.category == 'laser' || flipData.weapon.system.category == 'ballistic') && flipData.coverConsequence.system.severity != 'none')) { flipData.coverSeverityLevel = this.getConsequenceSeverityLevel(flipData.coverConsequence.system.severity) * 2; flipData.coverSeverityFlag = (flipData.coverSeverityLevel > 0); flipData.isMelee = (flipData.weapon.system.category == 'melee'); let melee = defender.items.find(item => item.type == 'skill' && item.name == 'Melee'); flipData.defenderMelee = melee.system.value; flipData.uniqId = foundry.utils.randomID(16); this.registry[flipData.uniqId] = flipData; let html = await renderTemplate('systems/foundryvtt-shadows-over-sol/templates/chat-damage-request-dodge.html', flipData); ChatMessage.create({ content: html, whisper: ChatMessage.getWhisperRecipients(flipData.target.actor.name).concat(ChatMessage.getWhisperRecipients("GM")) }); return; // Wait message response } } flipData.isReaction = false; this.takeWounds(flipData); } /* -------------------------------------------- */ static reactionCover(uniqId) { let flipData = this.registry[uniqId]; flipData.tn += flipData.coverSeverityLevel; flipData.isReaction = true; game.combat.decreaseAPFromActor(flipData.target.actor._id); SoSUtility.applyDamage(flipData); } /* -------------------------------------------- */ static reactionMelee(uniqId) { let flipData = this.registry[uniqId]; flipData.tn += flipData.defenderMelee; flipData.isReaction = true; game.combat.decreaseAPFromActor(flipData.target.actor._id); SoSUtility.applyDamage(flipData); } /* -------------------------------------------- */ static reactionHit(uniqId) { let flipData = this.registry[uniqId]; flipData.isReaction = true; SoSUtility.takeWounds(flipData); } /* -------------------------------------------- */ static takeWounds(flipData) { let defender = game.actors.get(flipData.target.actor._id); defender.applyWounds(flipData); } /* -------------------------------------------- */ static async processItemDropEvent(actorSheet, event) { let dragData = JSON.parse(event.dataTransfer.getData("text/plain")); const item = fromUuidSync(dragData.uuid) let dropId = $(event.target).parents(".item").attr("data-item-id"); // Only relevant if container drop let objectId = item.id console.log("ID", dragData, dropId, objectId) if (dragData.type == 'Item' && dropId) { actorSheet.actor.addObjectToContainer(objectId, dropId); } return true } }