diff --git a/images/astuce_acteur_arme.webp b/images/astuce_acteur_arme.webp deleted file mode 100644 index 8296fe2..0000000 Binary files a/images/astuce_acteur_arme.webp and /dev/null differ diff --git a/images/astuce_acteur_macro.webp b/images/astuce_acteur_macro.webp deleted file mode 100644 index 34d8e12..0000000 Binary files a/images/astuce_acteur_macro.webp and /dev/null differ diff --git a/module.json b/module.json index 9429702..0c67500 100644 --- a/module.json +++ b/module.json @@ -8,12 +8,13 @@ } ], "url": "https://gitea.palmenalzer.de/alzer/wfrp-4e-de", - "version": "8.6.116", + "version": "8.6.117", "esmodules": [ "modules/babele-register.js", "modules/addon-register.js", "modules/import-stat-2.js", - "modules/loadScripts.js" + "modules/loadScripts.js", + "modules/wh4_de.js" ], "styles": [ "patch-styles.css" diff --git a/modules/fr-actor-wfrp4e.js b/modules/fr-actor-wfrp4e.js deleted file mode 100644 index 74c288b..0000000 --- a/modules/fr-actor-wfrp4e.js +++ /dev/null @@ -1,3610 +0,0 @@ -/** - * Provides the main Actor data computation and organization. - * - * ActorWfrp4e contains all the preparation data and methods used for preparing an actor: - * going through each Owned Item, preparing them for display based on characteristics. - * Additionally, it handles all the different types of roll requests, setting up the - * test dialog, how each test is displayed, etc. - * - * - * @see ActorSheetWfrp4e - Base sheet class - * @see ActorSheetWfrp4eCharacter - Character sheet class - * @see ActorSheetWfrp4eNPC - NPC sheet class - * @see ActorSheetWfrp4eCreature - Creature sheet class - * @see DiceWFRP4e - Sends test data to roll tests. - */ -export default class ActorWfrp4e_fr extends Actor { - - /** - * Override the create() function to provide additional WFRP4e functionality. - * - * This overrided create() function adds initial items and flags to an actor - * upon creation. Namely: Basic skills, the 3 default coin values (brass - * pennies, silver shillings, gold crowns) at a quantity of 0, and setting - * up the default Automatic Calculation flags to be true. We still want to - * use the upstream create method, so super.create() is called at the end. - * Additionally - See the preCreateActor hook for more initial settings - * upon creation - * - * @param {Object} data Barebones actor data which this function adds onto. - * @param {Object} options (Unused) Additional options which customize the creation workflow. - * - */ - static async create(data, options) { - // If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic - if (data.items) { - return super.create(data, options); - } - - // Initialize empty items - data.items = []; - - // Default auto calculation to true - data.flags = - { - autoCalcRun: true, - autoCalcWalk: true, - autoCalcWounds: true, - autoCalcCritW: true, - autoCalcCorruption: true, - autoCalcEnc: true, - autoCalcSize: true, - } - let basicSkills = await game.wfrp4e.utility.allBasicSkills() || []; - let moneyItems = await game.wfrp4e.utility.allMoneyItems() || []; - moneyItems = moneyItems.sort((a, b) => (a.data.coinValue.value > b.data.coinValue.value) ? -1 : 1); - - // If character, automatically add basic skills and money items - if (data.type == "character") { - data.items = data.items.concat(basicSkills); - - // Set all money items to 0, add to actor - data.items = data.items.concat(moneyItems.map(m => { - m.data.quantity.value = 0 - return m - })) - super.create(data, options); // Follow through the the rest of the Actor creation process upstream - } - // If not a character, ask the user whether they want to add basic skills / money - else if (data.type == "npc" || data.type == "creature") { - new Dialog({ - title: game.i18n.localize("ACTOR.BasicSkillsTitle"), - content: `
${game.i18n.localize("ACTOR.BasicSkillsPrompt")}
`, - buttons: { - yes: { - label: game.i18n.localize("Yes"), - callback: async dlg => { - data.items = data.items.concat(basicSkills); - - // Set all money items to 0, add to actor - data.items = data.items.concat(moneyItems.map(m => { - m.data.quantity.value = 0 - return m - })) - super.create(data, options); // Follow through the the rest of the Actor creation process upstream - } - }, - no: { - label: game.i18n.localize("No"), - callback: async dlg => { - super.create(data, options); // Do not add new items, continue with the rest of the Actor creation process upstream - } - }, - }, - default: 'yes' - }).render(true); - } - } - - - - prepareBaseData() { - // For each characteristic, calculate the total and bonus value - for (let ch of Object.values(this.data.data.characteristics)) { - ch.value = ch.initial + ch.advances + (ch.modifier || 0); - ch.bonus = Math.floor(ch.value / 10) - ch.cost = game.wfrp4e.utility._calculateAdvCost(ch.advances, "characteristic") - } - } - - /** - * Calculates simple dynamic data when actor is updated. - * - * prepareData() is called when actor data is updated to recalculate values such as Characteristic totals, bonus (e.g. - * this is how Strength total and Strength Bonus gets updated whenever the user changes the Strength characteristic), - * movement values, and encumbrance. Some of these may or may not actually be calculated, depending on the user choosing - * not to have them autocalculated. These values are relatively simple, more complicated calculations that require items - * can be found in the sheet's getData() function. - * - * NOTE: NOT TO BE CONFUSED WITH prepare() - that function is called upon rendering to organize and process actor data - * - * @see ActorSheetWfrp4e.getData() - */ - prepareData() { - try { - super.prepareData(); - const data = this.data; - - if (this.data.type == "character") - this.prepareCharacter(); - if (this.data.type == "creature") - this.prepareCreature(); - - // Only characters have experience - if (data.type === "character") - data.data.details.experience.current = data.data.details.experience.total - data.data.details.experience.spent; - - // Auto calculation values - only calculate if user has not opted to enter ther own values - if (data.flags.autoCalcWalk) - data.data.details.move.walk = parseInt(data.data.details.move.value) * 2; - - if (data.flags.autoCalcRun) - data.data.details.move.run = parseInt(data.data.details.move.value) * 4; - - if (data.flags.autoCalcEnc) - data.data.status.encumbrance.max = data.data.characteristics.t.bonus + data.data.characteristics.s.bonus; - - if (game.settings.get("wfrp4e", "capAdvantageIB")) - data.data.status.advantage.max = data.data.characteristics.i.bonus - else - data.data.status.advantage.max = 10; - - - if (!hasProperty(this, "data.flags.autoCalcSize")) - data.flags.autoCalcSize = true; - - - // Find size based on Traits/Talents - let size; - let trait = data.items.find(t => t.type == "trait" && t.name.toLowerCase().includes(game.i18n.localize("NAME.Size").toLowerCase())); - if (this.data.type == "creature") { - trait = data.items.find(t => t.type == "trait" && t.included && t.name.toLowerCase().includes(game.i18n.localize("NAME.Size").toLowerCase())) - } - if (trait) - size = trait.data.specification.value; - else { - size = data.items.find(x => x.type == "talent" && x.name.toLowerCase() == game.i18n.localize("NAME.Small").toLowerCase()); - if (size) - size = size.name; - else - size = game.i18n.localize("SPEC.Average") - } - - // If the size has been changed since the last known value, update the value - data.data.details.size.value = game.wfrp4e.utility.findKey(size, game.wfrp4e.config.actorSizes) || "avg" - - // Now that we have size, calculate wounds and token size - if (data.flags.autoCalcWounds) { - let wounds = this._calculateWounds() - if (data.data.status.wounds.max != wounds) // If change detected, reassign max and current wounds - { - data.data.status.wounds.max = wounds; - data.data.status.wounds.value = wounds; - } - } - - if (data.flags.autoCalcSize) { - //let tokenSize = WFRP4E.tokenSizes[data.data.details.size.value] - let tokenSize = game.wfrp4e.config.tokenSizes[data.data.details.size.value] - if (this.isToken) { - this.token.update({"height" : tokenSize, "width" : tokenSize }); - } - data.token.height = tokenSize; - data.token.width = tokenSize; - } - - // Auto calculation flags - if the user hasn't disabled various autocalculated values, calculate them - if (data.flags.autoCalcRun) { - // This is specifically for the Stride trait - if (data.items.find(t => t.type == "trait" && t.name.toLowerCase() == game.i18n.localize("NAME.Stride").toLowerCase())) - data.data.details.move.run += data.data.details.move.walk; - } - - let talents = data.items.filter(t => t.type == "talent") - // talentTests is used to easily reference talent bonuses (e.g. in setupTest function and dialog) - // instead of iterating through every item again to find talents when rolling - data.flags.talentTests = []; - for (let talent of talents) // For each talent, if it has a Tests value, push it to the talentTests array - if (talent.data.tests.value) - data.flags.talentTests.push({ talentName: talent.name, test: talent.data.tests.value, SL: talent.data.advances.value }); - - // ------------------------ Talent Modifications ------------------------ - // These consist of Strike Mighty Blow, Accurate Shot, and Robust. Each determines - // how many advances there are according to preparedData, then modifies the flag value - // if there's any difference. - - // Strike Mighty Blow Talent - let smb = talents.filter(t => t.name.toLowerCase() == game.i18n.localize("NAME.SMB").toLowerCase()).reduce((advances, talent) => advances + talent.data.advances.value, 0) - if (smb) - data.flags.meleeDamageIncrease = smb - else if (!smb) - data.flags.meleeDamageIncrease = 0 - - // Accurate Shot Talent - let accshot = talents.filter(t => t.name.toLowerCase() == game.i18n.localize("NAME.AS").toLowerCase()).reduce((advances, talent) => advances + talent.data.advances.value, 0) - if (accshot) - data.flags.rangedDamageIncrease = accshot; - else if (!accshot) - data.flags.rangedDamageIncrease = 0 - - // Robust Talent - let robust = talents.filter(t => t.name.toLowerCase() == game.i18n.localize("NAME.Robust").toLowerCase()).reduce((advances, talent) => advances + talent.data.advances.value, 0) - if (robust) - data.flags.robust = robust; - else - data.flags.robust = 0 - - let ambi = talents.filter(t => t.name.toLowerCase() == game.i18n.localize("NAME.Ambi").toLowerCase()).reduce((advances, talent) => advances + talent.data.advances.value, 0) - data.flags.ambi = ambi; - - } - catch (error) { - console.error("Something went wrong with preparing actor data: " + error) - ui.notifications.error(game.i18n.localize("ACTOR.PreparationError") + error) - } - } - - /** - * Augments actor preparation with additional calculations for Characters. - * - * Characters have more features and so require more calculation. Specifically, - * this will add pure soul talent advances to max corruption, as well as display - * current career values (details, advancement indicatiors, etc.). - * - * Note that this functions requires actorData to be prepared, by this.prepare(). - * - * @param {Object} actorData prepared actor data to augment - */ - prepareCharacter() { - if (this.data.type != "character") - return; - - let tb = this.data.data.characteristics.t.bonus; - let wpb = this.data.data.characteristics.wp.bonus; - - // If the user has not opted out of auto calculation of corruption, add pure soul value - if (this.data.flags.autoCalcCorruption) { - this.data.data.status.corruption.max = tb + wpb; - let pureSoulTalent = this.data.items.find(x => x.type == "talent" && x.name.toLowerCase() == (game.i18n.localize("NAME.PS")).toLowerCase()) - if (pureSoulTalent) - this.data.data.status.corruption.max += pureSoulTalent.data.advances.value; - } - } - - - prepareNPC() { - - } - - - /** - * Augments actor preparation with additional calculations for Creatures. - * - * preparing for Creatures mainly involves excluding traits that were marked to be excluded, - * then replacing the traits array with only the included traits (which is used by prepare()). - * - * Note that this functions requires actorData to be prepared, by this.prepare(). - * - * @param {Object} actorData prepared actor data to augment - */ - prepareCreature() { - if (this.data.type != "creature") - return; - - // mark each trait as included or not - for (let trait of this.data.items.filter(i => i.type == "trait")) { - if (this.data.data.excludedTraits.includes(trait._id)) - trait.included = false; - else - trait.included = true; - } - - } - - /* --------------------------------------------------------------------------------------------------------- */ - /* Setting up Rolls - /* - /* All "setup______" functions gather the data needed to roll a certain test. These are in 3 main objects. - /* These 3 objects are then given to DiceWFRP.setupDialog() to show the dialog, see that function for its usage. - /* - /* The 3 Main objects: - /* testData - Data associated with modifications to rolling the test itself, or results of the test. - /* Examples of this are whether hit locations are found, Weapon qualities that may cause - criticals/fumbles more often or ingredients for spells that cancel miscasts. - dialogOptions - Data for rendering the dialog that's important for a specific test type. - Example: when casting or channelling, there should be an option for Malignant - Influences, but only for those tests. - cardOptions - Which card to use, the title of the card, the name of the actor, etc. - /* --------------------------------------------------------------------------------------------------------- */ - - /** - * Setup a Characteristic Test. - * - * Characteristics tests are the simplest test, all that needs considering is the target number of the - * characteristic being tested, and any modifiers the user enters. - * - * @param {String} characteristicId The characteristic id (e.g. "ws") - id's can be found in config.js - * - */ - setupCharacteristic(characteristicId, options = {}) { - let char = this.data.data.characteristics[characteristicId]; - let title = game.i18n.localize(char.label) + " " + game.i18n.localize("Test"); - - let testData = { - target: char.value, - hitLocation: false, - extra: { - size : this.data.data.details.size.value, - actor : this.data, - options: options - } - }; - - if (options.rest) - testData.extra.options["tb"] = char.bonus; - - // Default a WS or BS test to have hit location checked - if (characteristicId == "ws" || characteristicId == "bs") - testData.hitLocation = true; - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/characteristic-dialog.html", - // Prefilled dialog data - data: { - hitLocation: testData.hitLocation, - talents: this.data.flags.talentTests, - advantage: this.data.data.status.advantage.value || 0, - rollMode: options.rollMode - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - // Target value is the final value being tested against, after all modifiers and bonuses are added - testData.target = testData.target + testData.testModifier + testData.testDifficulty; - testData.hitLocation = html.find('[name="hitLocation"]').is(':checked'); - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - return { testData, cardOptions }; - } - }; - - if (options.corruption) { - title = `Corrupting Influence - ${game.i18n.localize(char.label)} Test` - dialogOptions.title = title; - dialogOptions.data.testDifficulty = "challenging" - } - if (options.mutate) { - title = `Dissolution of Body and Mind - ${game.i18n.localize(char.label)} Test` - dialogOptions.title = title; - dialogOptions.data.testDifficulty = "challenging" - } - - if (options.rest) { - dialogOptions.data.testDifficulty = "average" - } - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/characteristic-card.html", title) - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - /** - * Setup a Skill Test. - * - * Skill tests are much like Characteristic Tests in their simplicity, just with another layer of modifiers (skill advances). - * However, there is more complication if the skill is instead for an Income test, which adds computation after the roll is - * completed. - * - * @param {Object} skill The skill item being tested. Skill items contain the advancements and the base characteristic, see template.json for more information. - * @param {bool} income Whether or not the skill is being tested to determine Income. - */ - setupSkill(skill, options = {}) { - let title = skill.name + " " + game.i18n.localize("Test"); - let testData = { - hitLocation: false, - income: options.income, - target: this.data.data.characteristics[skill.data.characteristic.value].value + skill.data.advances.value, - extra: { - size: this.data.data.details.size.value, - actor : this.data, - options: options, - skill: skill - } - }; - - // Default a WS, BS, Melee, or Ranged to have hit location checked - if (skill.data.characteristic.value == "ws" || - skill.data.characteristic.value == "bs" || - skill.name.includes(game.i18n.localize("NAME.Melee")) || - skill.name.includes(game.i18n.localize("NAME.Ranged"))) { - testData.hitLocation = true; - } - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/skill-dialog.html", - // Prefilled dialog data - - data: { - hitLocation: testData.hitLocation, - talents: this.data.flags.talentTests, - characteristicList: game.wfrp4e.config.characteristics, - characteristicToUse: skill.data.characteristic.value, - advantage: this.data.data.status.advantage.value || 0, - rollMode: options.rollMode - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - let characteristicToUse = html.find('[name="characteristicToUse"]').val(); - // Target value is the final value being tested against, after all modifiers and bonuses are added - testData.target = - this.data.data.characteristics[characteristicToUse].value - + testData.testModifier - + testData.testDifficulty - + skill.data.advances.value - + skill.data.modifier.value - - testData.hitLocation = html.find('[name="hitLocation"]').is(':checked'); - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - - return { testData, cardOptions }; - } - }; - - // If Income, use the specialized income roll handler and set testDifficulty to average - if (testData.income) { - dialogOptions.data.testDifficulty = "average"; - } - if (options.corruption) { - title = `Corrupting Influence - ${skill.name} Test` - dialogOptions.title = title; - dialogOptions.data.testDifficulty = "challenging" - } - if (options.mutate) { - title = `Dissolution of Body and Mind - ${skill.name} Test` - dialogOptions.title = title; - dialogOptions.data.testDifficulty = "challenging" - } - - // If Rest & Recover, set testDifficulty to average - if (options.rest) { dialogOptions.data.testDifficulty = "average"; } - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/skill-card.html", title) - if (options.corruption) - cardOptions.rollMode = "gmroll" - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - /** - * Setup a Weapon Test. - * - * Probably the most complicated type of Test, weapon tests' complexity comes from all the different - * factors and variables of the different weapons available and how they might affect test results, - * as well as ammo usage, the effects of using different skills etc. - * - * @param {Object} weapon The weapon Item being used. - * @param {bool} event The event that called this Test, used to determine if attack is melee or ranged. - */ - setupWeapon(weapon, options = {}) { - let skillCharList = []; // This array is for the different options available to roll the test (Skills and characteristics) - let slBonus = 0 // Used when wielding Defensive weapons - let modifier = 0; // Used when attacking with Accurate weapons - let successBonus = 0; - let title = game.i18n.localize("WeaponTest") + " - " + weapon.name; - - // Prepare the weapon to have the complete data object, including qualities/flaws, damage value, etc. - let wep = this.prepareWeaponCombat(foundry.utils.duplicate(weapon)); - let testData = { - target: 0, - hitLocation: true, - extra: { // Store this extra weapon/ammo data for later use - weapon: wep, - size: this.data.data.details.size.value, - actor : this.data, - champion: !!this.items.find(i => i.data.name.toLowerCase() == game.i18n.localize("NAME.Champion").toLowerCase() && i.type == "trait"), - riposte: !!this.items.find(i => i.data.name.toLowerCase() == game.i18n.localize("NAME.Riposte").toLowerCase() && i.type == "talent"), - options: options - } - }; - - if (wep.attackType == "melee") - skillCharList.push(game.i18n.localize("Weapon Skill")) - - else if (wep.attackType == "ranged") { - // If Ranged, default to Ballistic Skill, but check to see if the actor has the specific skill for the weapon - skillCharList.push(game.i18n.localize("Ballistic Skill")) - if (weapon.data.weaponGroup.value != "throwing" && weapon.data.weaponGroup.value != "explosives" && weapon.data.weaponGroup.value != "entangling") { - // Check to see if they have ammo if appropriate - testData.extra.ammo = foundry.utils.duplicate(this.getEmbeddedEntity("OwnedItem", weapon.data.currentAmmo.value)) - if (!testData.extra.ammo || weapon.data.currentAmmo.value == 0 || testData.extra.ammo.data.quantity.value == 0) { - AudioHelper.play({ src: "systems/wfrp4e/sounds/no.wav" }, false) - ui.notifications.error(game.i18n.localize("Error.NoAmmo")) - return - } - } - else if (weapon.data.weaponGroup.value != "entangling" && weapon.data.quantity.value == 0) { - // If this executes, it means it uses its own quantity for ammo (e.g. throwing), which it has none of - AudioHelper.play({ src: "systems/wfrp4e/sounds/no.wav" }, false) - ui.notifications.error(game.i18n.localize("Error.NoAmmo")) - return; - } - else { - // If this executes, it means it uses its own quantity for ammo (e.g. throwing) - testData.extra.ammo = weapon; - } - } - - let defaultSelection // The default skill/characteristic being used - if (wep.skillToUse) { - // If the actor has the appropriate skill, default to that. - skillCharList.push(wep.skillToUse.name) - defaultSelection = skillCharList.indexOf(wep.skillToUse.name) - testData.target = this.data.data.characteristics[wep.skillToUse.data.characteristic.value].value + wep.skillToUse.data.advances.value; - - } - - // Bypass macro default values - if (!testData.target) - testData.target = wep.attackType == "melee" ? this.data.data.characteristics["ws"].value : this.data.data.characteristics["bs"].value - - // ***** Automatic Test Data Fill Options ****** - - // If offhand and should apply offhand penalty (should apply offhand penalty = not parry, not defensive, and not twohanded) - if (getProperty(wep, "data.offhand.value") && !wep.data.twohanded.value && !(weapon.data.weaponGroup.value == "parry" && wep.properties.qualities.includes(game.i18n.localize("PROPERTY.Defensive")))) - { - modifier = -20 - modifier += Math.min(20, this.data.flags.ambi * 10) - } - - // Try to automatically fill the dialog with values based on context - // If the auto-fill setting is true, and there is combat.... - if (game.settings.get("wfrp4e", "testAutoFill") && (game.combat && game.combat.data.round != 0 && game.combat.turns)) { - try { - let currentTurn = game.combat.turns[game.combat.current.turn] - - - // If actor is a token - if (this.data.token.actorLink) { - // If it is NOT the actor's turn - if (currentTurn && this.data.token != currentTurn.token) - slBonus = this.data.flags.defensive; // Prefill Defensive values (see prepareItems() for how defensive flags are assigned) - - else // If it is the actor's turn - { - // Prefill dialog according to qualities/flaws - if (wep.properties.qualities.includes(game.i18n.localize("PROPERTY.Accurate"))) - modifier += 10; - if (wep.properties.qualities.includes(game.i18n.localize("PROPERTY.Precise"))) - successBonus += 1; - if (wep.properties.flaws.includes(game.i18n.localize("PROPERTY.Imprecise"))) - slBonus -= 1; - } - } - else // If the actor is not a token - { - // If it is NOT the actor's turn - if (currentTurn && currentTurn.tokenId != this.token.data._id) - slBonus = this.data.flags.defensive; - - else // If it is the actor's turn - { - // Prefill dialog according to qualities/flaws - if (wep.properties.qualities.includes(game.i18n.localize("PROPERTY.Accurate"))) - modifier += 10; - if (wep.properties.qualities.includes(game.i18n.localize("PROPERTY.Precise"))) - successBonus += 1; - if (wep.properties.flaws.includes(game.i18n.localize("PROPERTY.Imprecise"))) - slBonus -= 1; - } - } - } - catch // If something went wrong, default to 0 for all prefilled data - { - slBonus = 0; - successBonus = 0; - modifier = 0; - } - } - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/weapon-dialog.html", - // Prefilled dialog data - data: { - hitLocation: testData.hitLocation, - talents: this.data.flags.talentTests, - skillCharList: skillCharList, - slBonus: slBonus || 0, - successBonus: successBonus || 0, - testDifficulty: options.difficulty, - modifier: modifier || 0, - defaultSelection: defaultSelection, - advantage: this.data.data.status.advantage.value || 0, - rollMode: options.rollMode - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - let skillSelected = skillCharList[Number(html.find('[name="skillSelected"]').val())]; - - // Determine final target if a characteristic was selected - if (skillSelected == game.i18n.localize("CHAR.WS") || skillSelected == game.i18n.localize("CHAR.BS")) { - if (skillSelected == game.i18n.localize("CHAR.WS")) - testData.target = this.data.data.characteristics.ws.value - else if (skillSelected == game.i18n.localize("CHAR.BS")) - testData.target = this.data.data.characteristics.bs.value - - testData.target += testData.testModifier + testData.testDifficulty; - } - else // If a skill was selected - { - // If using the appropriate skill, set the target number to characteristic value + advances + modifiers - // Target value is the final value being tested against, after all modifiers and bonuses are added - let skillUsed = testData.extra.weapon.skillToUse; - - testData.target = - this.data.data.characteristics[skillUsed.data.characteristic.value].value - + testData.testModifier - + testData.testDifficulty - + skillUsed.data.advances.value - + skillUsed.data.modifier.value - } - - testData.hitLocation = html.find('[name="hitLocation"]').is(':checked'); - - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - - return { testData, cardOptions }; - } - - }; - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/weapon-card.html", title) - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - - /** - * Setup a Casting Test. - * - * Casting tests are more complicated due to the nature of spell miscasts, ingredients, etc. Whatever ingredient - * is selected will automatically be used and negate one miscast. For the spell rolling logic, see DiceWFRP.rollCastTest - * where all this data is passed to in order to calculate the roll result. - * - * @param {Object} spell The spell Item being Casted. The spell item has information like CN, lore, and current ingredient ID - * - */ - setupCast(spell, options = {}) { - let title = game.i18n.localize("CastingTest") + " - " + spell.name; - - // castSkill array holds the available skills/characteristics to cast with - Casting: Intelligence - let castSkills = [{ key: "int", name: game.i18n.localize("CHAR.Int") }] - - // if the actor has Language (Magick), add it to the array. - castSkills = castSkills.concat(this.items.filter(i => i.name.toLowerCase() == `${game.i18n.localize("Language")} (${game.i18n.localize("Magick")})`.toLowerCase() && i.type == "skill")) - - // Default to Language Magick if it exists - let defaultSelection = castSkills.findIndex(i => i.name.toLowerCase() == `${game.i18n.localize("Language")} (${game.i18n.localize("Magick")})`.toLowerCase()) - - // Whether the actor has Instinctive Diction is important in the test rolling logic - let instinctiveDiction = (this.data.flags.talentTests.findIndex(x => x.talentName.toLowerCase() == game.i18n.localize("NAME.ID").toLowerCase()) > -1) // instinctive diction boolean - - // Prepare the spell to have the complete data object, including damage values, range values, CN, etc. - let preparedSpell = this.prepareSpellOrPrayer(spell); - let testData = { - target: 0, - extra: { // Store this data to be used by the test logic - spell: preparedSpell, - malignantInfluence: false, - ingredient: false, - ID: instinctiveDiction, - size: this.data.data.details.size.value, - actor : this.data, - options: options - } - }; - - // If the spell does damage, default the hit location to checked - if (preparedSpell.damage) - testData.hitLocation = true; - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/spell-dialog.html", - // Prefilled dialog data - data: { - hitLocation: testData.hitLocation, - malignantInfluence: testData.malignantInfluence, - talents: this.data.flags.talentTests, - advantage: this.data.data.status.advantage.value || 0, - defaultSelection: defaultSelection, - castSkills: castSkills, - rollMode: options.rollMode - - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - - let skillSelected = castSkills[Number(html.find('[name="skillSelected"]').val())]; - - // If an actual skill (Language Magick) was selected, use that skill to calculate the target number - if (skillSelected.key != "int") { - testData.target = this.data.data.characteristics[skillSelected.data.data.characteristic.value].value - + skillSelected.data.data.advances.value - + skillSelected.data.data.modifier.value - + testData.testDifficulty - + testData.testModifier; - } - else // if a characteristic was selected, use just the characteristic - { - testData.target = this.data.data.characteristics.int.value - + testData.testDifficulty - + testData.testModifier; - } - - testData.hitLocation = html.find('[name="hitLocation"]').is(':checked'); - testData.extra.malignantInfluence = html.find('[name="malignantInfluence"]').is(':checked'); - - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - - return { testData, cardOptions }; - } - }; - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/spell-card.html", title) - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - /** - * Setup a Channelling Test. - * - * Channelling tests are more complicated due to the nature of spell miscasts, ingredients, etc. Whatever ingredient - * is selected will automatically be used and mitigate miscasts. For the spell rolling logic, see DiceWFRP.rollChannellTest - * where all this data is passed to in order to calculate the roll result. - * - * @param {Object} spell The spell Item being Channelled. The spell item has information like CN, lore, and current ingredient ID - * This spell SL will then be updated accordingly. - * - */ - setupChannell(spell, options = {}) { - let title = game.i18n.localize("ChannellingTest") + " - " + spell.name; - - // channellSkills array holds the available skills/characteristics to with - Channelling: Willpower - let channellSkills = [{ key: "wp", name: game.i18n.localize("CHAR.WP") }] - - // if the actor has any channel skills, add them to the array. - channellSkills = channellSkills.concat(this.items.filter(i => i.name.toLowerCase().includes(game.i18n.localize("NAME.Channelling").toLowerCase()) && i.type == "skill")) - - // Find the spell lore, and use that to determine the default channelling selection - let spellLore = spell.data.lore.value; - let defaultSelection - if (spell.data.wind && spell.data.wind.value) { - defaultSelection = channellSkills.indexOf(channellSkills.find(x => x.name.includes(spell.data.wind.value))) - if (defaultSelection == -1) { - let customChannellSkill = this.items.find(i => i.name.toLowerCase().includes(spell.data.wind.value.toLowerCase()) && i.type == "skill"); - if (customChannellSkill) { - channellSkills.push(customChannellSkill) - defaultSelection = channellSkills.length - 1 - } - } - } - else { - defaultSelection = channellSkills.indexOf(channellSkills.find(x => x.name.includes(game.wfrp4e.config.magicWind[spellLore]))); - } - - if (spellLore == "witchcraft") - defaultSelection = channellSkills.indexOf(channellSkills.find(x => x.name.toLowerCase().includes(game.i18n.localize("NAME.Channelling").toLowerCase()))) - - // Whether the actor has Aethyric Attunement is important in the test rolling logic - let aethyricAttunement = (this.data.flags.talentTests.findIndex(x => x.talentName.toLowerCase() == game.i18n.localize("NAME.AA").toLowerCase()) > -1) // aethyric attunement boolean - - let testData = { - target: 0, - extra: { // Store data to be used by the test logic - spell: this.prepareSpellOrPrayer(spell), - malignantInfluence: false, - actor : this.data, - ingredient: false, - AA: aethyricAttunement, - size: this.data.data.details.size.value, - options: options - } - }; - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/channel-dialog.html", - // Prefilled dialog data - data: { - malignantInfluence: testData.malignantInfluence, - channellSkills: channellSkills, - defaultSelection: defaultSelection, - talents: this.data.flags.talentTests, - advantage: "N/A", - rollMode: options.rollMode - - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - testData.extra.malignantInfluence = html.find('[name="malignantInfluence"]').is(':checked'); - - let skillSelected = channellSkills[Number(html.find('[name="skillSelected"]').val())]; - // If an actual Channelling skill was selected, use that skill to calculate the target number - if (skillSelected.key != "wp") { - testData.target = testData.testModifier + testData.testDifficulty - + this.data.data.characteristics[skillSelected.data.data.characteristic.value].value - + skillSelected.data.data.advances.value - + skillSelected.data.data.modifier.value - testData.extra.channellSkill = skillSelected.data - } - else // if the ccharacteristic was selected, use just the characteristic - testData.target = testData.testModifier + testData.testDifficulty + this.data.data.characteristics.wp.value - - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - - return { testData, cardOptions }; - - } - }; - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/channel-card.html", title) - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - /** - * Setup a Prayer Test. - * - * Prayer tests are fairly simple, with the main complexity coming from sin and wrath of the gods, - * the logic of which can be found in DiceWFRP.rollPrayerTest, where all this data here is passed - * to in order to calculate the roll result. - * - * @param {Object} prayer The prayer Item being used, compared to spells, not much information - * from the prayer itself is needed. - */ - setupPrayer(prayer, options = {}) { - let title = game.i18n.localize("PrayerTest") + " - " + prayer.name; - - // ppraySkills array holds the available skills/characteristics to pray with - Prayers: Fellowship - let praySkills = [{ key: "fel", name: game.i18n.localize("CHAR.Fel") }] - - // if the actor has the Pray skill, add it to the array. - praySkills = praySkills.concat(this.items.filter(i => i.name.toLowerCase() == game.i18n.localize("NAME.Pray").toLowerCase() && i.type == "skill")); - - // Default to Pray skill if available - let defaultSelection = praySkills.findIndex(i => i.name.toLowerCase() == game.i18n.localize("NAME.Pray").toLowerCase()) - - // Prepare the prayer to have the complete data object, including damage values, range values, etc. - let preparedPrayer = this.prepareSpellOrPrayer(prayer); - let testData = { // Store this data to be used in the test logic - target: 0, - hitLocation: false, - target: defaultSelection != -1 ? this.data.data.characteristics[praySkills[defaultSelection].data.data.characteristic.value].value + praySkills[defaultSelection].data.data.advances.value : this.data.data.characteristics.fel.value, - extra: { - prayer: preparedPrayer, - size: this.data.data.details.size.value, - actor : this.data, - sin: this.data.data.status.sin.value, - options: options, - rollMode: options.rollMode - } - }; - - - // If the spell does damage, default the hit location to checked - if (preparedPrayer.damage) - testData.hitLocation = true; - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/prayer-dialog.html", - // Prefilled dialog data - data: { - hitLocation: testData.hitLocation, - talents: this.data.flags.talentTests, - advantage: this.data.data.status.advantage.value || 0, - praySkills: praySkills, - defaultSelection: defaultSelection - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - - let skillSelected = praySkills[Number(html.find('[name="skillSelected"]').val())]; - // If an actual skill (Pray) was selected, use that skill to calculate the target number - if (skillSelected.key != "fel") { - testData.target = this.data.data.characteristics[skillSelected.data.data.characteristic.value].value - + skillSelected.data.data.advances.value - + testData.testDifficulty - + testData.testModifier; - + skillSelected.data.data.modifier.value - } - else // if a characteristic was selected, use just the characteristic - { - testData.target = this.data.data.characteristics.fel.value - + testData.testDifficulty - + testData.testModifier; - } - - testData.hitLocation = html.find('[name="hitLocation"]').is(':checked'); - - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - - return { testData, cardOptions }; - } - }; - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/prayer-card.html", title) - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - /** - * Setup a Trait Test. - * - * Some traits are rollable, and so are assigned a rollable characteristic, this is where - * rolling those characteristics is setup. Additonally, sometimes these traits have a - * "Bonus characteristic" which in most all cases means what characteristic bonus to add - * to determine damage. See the logic in traitTest. - * - * @param {Object} trait The trait Item being used, containing which characteristic/bonus characteristic to use - */ - setupTrait(trait, options = {}) { - if (!trait.data.rollable.value) - return; - let title = game.wfrp4e.config.characteristics[trait.data.rollable.rollCharacteristic] + ` ${game.i18n.localize("Test")} - ` + trait.name; - let testData = { - hitLocation: false, - target: this.data.data.characteristics[trait.data.rollable.rollCharacteristic].value, - extra: { // Store this trait data for later use - trait: trait, - size: this.data.data.details.size.value, - actor : this.data, - champion: !!this.items.find(i => i.data.name.toLowerCase() == game.i18n.localize("NAME.Champion").toLowerCase()), - options: options, - rollMode: options.rollMode - } - }; - - // Default hit location checked if the rollable trait's characteristic is WS or BS - if (trait.data.rollable.rollCharacteristic == "ws" || trait.data.rollable.rollCharacteristic == "bs") - testData.hitLocation = true; - - // Setup dialog data: title, template, buttons, prefilled data - let dialogOptions = { - title: title, - template: "/systems/wfrp4e/templates/dialog/skill-dialog.html", // Reuse skill dialog - // Prefilled dialog data - data: { - hitLocation: testData.hitLocation, - talents: this.data.flags.talentTests, - characteristicList: game.wfrp4e.config.characteristics, - characteristicToUse: trait.data.rollable.rollCharacteristic, - advantage: this.data.data.status.advantage.value || 0, - testDifficulty: trait.data.rollable.defaultDifficulty - }, - callback: (html) => { - // When dialog confirmed, fill testData dialog information - // Note that this does not execute until DiceWFRP.setupDialog() has finished and the user confirms the dialog - cardOptions.rollMode = html.find('[name="rollMode"]').val(); - testData.testModifier = Number(html.find('[name="testModifier"]').val()); - testData.testDifficulty = game.wfrp4e.config.difficultyModifiers[html.find('[name="testDifficulty"]').val()]; - testData.successBonus = Number(html.find('[name="successBonus"]').val()); - testData.slBonus = Number(html.find('[name="slBonus"]').val()); - let characteristicToUse = html.find('[name="characteristicToUse"]').val(); - // Target value is the final value being tested against, after all modifiers and bonuses are added - testData.target = this.data.data.characteristics[characteristicToUse].value - + testData.testModifier - + testData.testDifficulty - testData.hitLocation = html.find('[name="hitLocation"]').is(':checked'); - let talentBonuses = html.find('[name = "talentBonuses"]').val(); - - // Combine all Talent Bonus values (their times taken) into one sum - testData.successBonus += talentBonuses.reduce(function (prev, cur) { - return prev + Number(cur) - }, 0) - - return { testData, cardOptions }; - } - }; - - // Call the universal cardOptions helper - let cardOptions = this._setupCardOptions("systems/wfrp4e/templates/chat/roll/skill-card.html", title) - - // Provide these 3 objects to setupDialog() to create the dialog and assign the roll function - return game.wfrp4e.dice.setupDialog({ - dialogOptions: dialogOptions, - testData: testData, - cardOptions: cardOptions - }); - } - - - /** - * Universal card options for setup functions. - * - * The setup_____() functions all use the same cardOptions, just different templates. So this is - * a standardized helper function to maintain DRY code. - * - * @param {string} template Fileptah to the template being used - * @param {string} title Title of the Test to be displayed on the dialog and card - */ - _setupCardOptions(template, title) { - let cardOptions = { - speaker: { - alias: this.data.token.name, - actor: this.data._id, - }, - title: title, - template: template, - flags: { img: this.data.token.randomImg ? this.data.img : this.data.token.img } - // img to be displayed next to the name on the test card - if it's a wildcard img, use the actor image - } - - // If the test is coming from a token sheet - if (this.token) { - cardOptions.speaker.alias = this.token.data.name; // Use the token name instead of the actor name - cardOptions.speaker.token = this.token.data._id; - cardOptions.speaker.scene = canvas.scene._id - cardOptions.flags.img = this.token.data.img; // Use the token image instead of the actor image - } - else // If a linked actor - use the currently selected token's data if the actor id matches - { - let speaker = ChatMessage.getSpeaker() - if (speaker.actor == this.data._id) { - cardOptions.speaker.alias = speaker.alias - cardOptions.speaker.token = speaker.token - cardOptions.speaker.scene = speaker.scene - cardOptions.flags.img = speaker.token ? canvas.tokens.get(speaker.token).data.img : cardOptions.flags.img - } - } - - return cardOptions - } - - /* --------------------------------------------------------------------------------------------------------- */ - /* --------------------------------------------- Roll Overides --------------------------------------------- */ - /* --------------------------------------------------------------------------------------------------------- */ - /** - * Roll overrides are specialized functions for different types of rolls. In each override, DiceWFRP is called - * to perform the test logic, which has its own specialized functions for different types of tests. For exapmle, - * weaponTest() calls DiceWFRP.rollWeaponTest(). Additionally, any post-roll logic that needs to be performed - * is done here. For example, Income tests use incomeTest, which determines how much money is made after the - * roll is completed. A normal Skill Test does not go through this process, instead using basicTest override, - * however both overrides just use the standard DiceWFRP.rollTest(). - * - /* --------------------------------------------------------------------------------------------------------- */ - - /** - * Default Roll override, the standard rolling method for general tests. - * - * basicTest is the default roll override (see DiceWFRP.setupDialog() for where it's assigned). This follows - * the basic steps. Call DiceWFRP.rollTest for standard test logic, send the result and display data to - * if(!options.suppressMessage) -DiceWFRP.renderRollCard() as well as handleOpposedTarget(). - * - * @param {Object} testData All the data needed to evaluate test results - see setupSkill/Characteristic - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async basicTest({ testData, cardOptions }, options = {}) { - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollTest(testData); - - result.postFunction = "basicTest"; - if (testData.extra) - foundry.utils.mergeObject(result, testData.extra); - - if (result.options.corruption) { - this.handleCorruptionResult(result); - } - if (result.options.mutate) { - this.handleMutationResult(result) - } - - if (result.options.extended) - { - this.handleExtendedTest(result) - } - - try { - let contextAudio = await game.wfrp4e.audio.MatchContextAudio(game.wfrp4e.audio.FindContext(result)) - cardOptions.sound = contextAudio.file || cardOptions.sound - } - catch - { } - Hooks.call("wfrp4e:rollTest", result, cardOptions) - - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`; - cardOptions.isOpposedTest = true - } - - if (!options.suppressMessage) - if (!options.suppressMessage) - game.wfrp4e.dice.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) // Send to handleOpposed to determine opposed status, if any. - }) - return {result, cardOptions}; - } - - /** - * incomeTest is used to add income calculation to Skill tests. - * - * Normal skill Tests just use basicTest() override, however, when testing Income, this override is used instead - * because it adds 'post processing' in the form of determining how much money was earned. See this.setupSkill() - * for how this override is assigned. - * - * @param {Object} testData All the data needed to evaluate test results - see setupSkill() - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async incomeTest({ testData, cardOptions }, options = {}) { - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollTest(testData); - result.postFunction = "incomeTest" - - Hooks.call("wfrp4e:rollIncomeTest", result, cardOptions) - - - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`, - cardOptions.isOpposedTest = true - } - - let status = testData.income.value.split(' ') - - let dieAmount = game.wfrp4e.config.earningValues[game.wfrp4e.utility.findKey(status[0], game.wfrp4e.config.statusTiers)][0] // b, s, or g maps to 2d10, 1d10, or 1 respectively (takes the first letter) - dieAmount = Number(dieAmount) * status[1]; // Multilpy that first letter by your standing (Brass 4 = 8d10 pennies) - let moneyEarned; - if (game.wfrp4e.utility.findKey(status[0], game.wfrp4e.config.statusTiers) != "g") // Don't roll for gold, just use standing value - { - dieAmount = dieAmount + "d10"; - moneyEarned = new Roll(dieAmount).roll().total; - } - else - moneyEarned = dieAmount; - - // After rolling, determined how much, if any, was actually earned - if (result.description.includes("Success")) { - result.incomeResult = game.i18n.localize("INCOME.YouEarn") + " " + moneyEarned; - switch (game.wfrp4e.utility.findKey(status[0], game.wfrp4e.config.statusTiers)) { - case "b": - result.incomeResult += ` ${game.i18n.localize("NAME.BPPlural").toLowerCase()}.` - break; - case "s": - result.incomeResult += ` ${game.i18n.localize("NAME.SSPlural").toLowerCase()}.` - break; - case "g": - if (moneyEarned > 1) - result.incomeResult += ` ${game.i18n.localize("NAME.GC").toLowerCase()}.` - else - result.incomeResult += ` ${game.i18n.localize("NAME.GCPlural").toLowerCase()}.` - break; - } - } - else if (Number(result.SL) > -6) { - moneyEarned /= 2; - result.incomeResult = game.i18n.localize("INCOME.YouEarn") + " " + moneyEarned; - switch (game.wfrp4e.utility.findKey(status[0], game.wfrp4e.config.statusTiers)) { - case "b": - result.incomeResult += ` ${game.i18n.localize("NAME.BPPlural").toLowerCase()}.` - break; - case "s": - result.incomeResult += ` ${game.i18n.localize("NAME.SSPlural").toLowerCase()}.` - break; - case "g": - if (moneyEarned > 1) - result.incomeResult += ` ${game.i18n.localize("NAME.GC").toLowerCase()}.` - else - result.incomeResult += ` ${game.i18n.localize("NAME.GCPlural").toLowerCase()}.` - break; - } - } - else { - result.incomeResult = game.i18n.localize("INCOME.Failure") - moneyEarned = 0; - } - // let contextAudio = await WFRP_Audio.MatchContextAudio(WFRP_Audio.FindContext(result)) - // cardOptions.sound = contextAudio.file || cardOptions.sound - result.moneyEarned = moneyEarned + game.wfrp4e.utility.findKey(status[0], game.wfrp4e.config.statusTiers); - if (!options.suppressMessage) - game.wfrp4e.dice.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) - }) - return {result, cardOptions}; - } - - /** - * weaponTest is used for weapon tests, see setupWeapon for how it's assigned. - * - * weaponTest doesn't add any special functionality, it's main purpose being to call - * DiceWFRP.rollWeaponTest() instead of the generic DiceWFRP.rollTest() - * - * @param {Object} testData All the data needed to evaluate test results - see setupWeapon() - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async weaponTest({ testData, cardOptions }, options = {}) { - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`, - cardOptions.isOpposedTest = true - } - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollWeaponTest(testData); - result.postFunction = "weaponTest"; - - // Reduce ammo if necessary - if (result.ammo && result.weapon.data.weaponGroup.value != game.i18n.localize("SPEC.Entangling").toLowerCase()) { - result.ammo.data.quantity.value--; - this.updateEmbeddedEntity("OwnedItem", { _id: result.ammo._id, "data.quantity.value": result.ammo.data.quantity.value }); - } - - try { - let contextAudio = await game.wfrp4e.audio.MatchContextAudio(game.wfrp4e.audio.FindContext(result)) - cardOptions.sound = contextAudio.file || cardOptions.sound - } - catch - { } - Hooks.call("wfrp4e:rollWeaponTest", result, cardOptions) - - - if (!options.suppressMessage) - game.wfrp4e.dice.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) // Send to handleOpposed to determine opposed status, if any. - }) - return {result, cardOptions}; - } - - /** - * castTest is used for casting tests, see setupCast for how it's assigned. - * - * The only special functionality castTest adds is reseting spell SL channelled back to 0, other than that, - * it's main purpose is to call DiceWFRP.rollCastTest() instead of the generic DiceWFRP.rollTest(). - * - * @param {Object} testData All the data needed to evaluate test results - see setupCast() - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async castTest({ testData, cardOptions }, options = {}) { - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`, - cardOptions.isOpposedTest = true - } - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollCastTest(testData); - result.postFunction = "castTest"; - - - // Find ingredient being used, if any - let ing = foundry.utils.duplicate(this.getEmbeddedEntity("OwnedItem", testData.extra.spell.data.currentIng.value)) - if (ing) { - // Decrease ingredient quantity - testData.extra.ingredient = true; - ing.data.quantity.value--; - this.updateEmbeddedEntity("OwnedItem", ing); - } - // If quantity of ingredient is 0, disregard the ingredient - else if (!ing || ing.data.data.quantity.value <= 0) - testData.extra.ingredient = false; - - try { - let contextAudio = await game.wfrp4e.audio.MatchContextAudio(game.wfrp4e.audio.FindContext(result)) - cardOptions.sound = contextAudio.file || cardOptions.sound - } - catch - { } - Hooks.call("wfrp4e:rollCastTest", result, cardOptions) - - - // Update spell to reflect SL from channelling resetting to 0 - game.wfrp4e.utility.getSpeaker(cardOptions.speaker).updateEmbeddedEntity("OwnedItem", { _id: testData.extra.spell._id, 'data.cn.SL': 0 }); - - - if (!options.suppressMessage) - game.wfrp4e.dice.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) // Send to handleOpposed to determine opposed status, if any. - }) - return {result, cardOptions}; - } - - /** - * channelTest is used for casting tests, see setupCast for how it's assigned. - * - * channellOveride doesn't add any special functionality, it's main purpose being to call - * DiceWFRP.rollChannellTest() instead of the generic DiceWFRP.rollTest() - * - * @param {Object} testData All the data needed to evaluate test results - see setupChannell() - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async channelTest({ testData, cardOptions }, options = {}) { - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`, - cardOptions.isOpposedTest = true - } - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollChannellTest(testData, game.wfrp4e.utility.getSpeaker(cardOptions.speaker)); - result.postFunction = "channelTest"; - - // Find ingredient being used, if any - let ing = foundry.utils.duplicate(this.getEmbeddedEntity("OwnedItem", testData.extra.spell.data.currentIng.value)) - if (ing) { - // Decrease ingredient quantity - testData.extra.ingredient = true; - ing.data.quantity.value--; - this.updateEmbeddedEntity("OwnedItem", ing); - } - // If quantity of ingredient is 0, disregard the ingredient - else if (!ing || ing.data.data.quantity.value <= 0) - testData.extra.ingredient = false; - - try { - let contextAudio = await game.wfrp4e.audio.MatchContextAudio(game.wfrp4e.audio.FindContext(result)) - cardOptions.sound = contextAudio.file || cardOptions.sound - } - catch - { } - Hooks.call("wfrp4e:rollChannelTest", result, cardOptions) - - if (!options.suppressMessage) - DiceWFRP.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) // Send to handleOpposed to determine opposed status, if any. - }) - return {result, cardOptions}; - } - - /** - * prayerTest is used for casting tests, see setupCast for how it's assigned. - * - * prayerTest doesn't add any special functionality, it's main purpose being to call - * DiceWFRP.rollPrayerTest() instead of the generic DiceWFRP.rollTest() - * - * @param {Object} testData All the data needed to evaluate test results - see setupPrayer() - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async prayerTest({ testData, cardOptions }, options = {}) { - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`, - cardOptions.isOpposedTest = true - } - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollPrayTest(testData, game.wfrp4e.utility.getSpeaker(cardOptions.speaker)); - result.postFunction = "prayerTest"; - - try { - let contextAudio = await game.wfrp4e.audio.MatchContextAudio(game.wfrp4e.audio.FindContext(result)) - cardOptions.sound = contextAudio.file || cardOptions.sound - } - catch - { } - Hooks.call("wfrp4e:rollPrayerTest", result, cardOptions) - - if (!options.suppressMessage) - game.wfrp4e.dice.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) // Send to handleOpposed to determine opposed status, if any. - }) - return {result, cardOptions}; - } - - /** - * traitTest is used for Trait tests, see setupTrait for how it's assigned. - * - * Since traitTest calls the generic DiceWFRP.rollTest(), which does not consider damage, - * some post processing must be done to calculate damage values. - * - * @param {Object} testData All the data needed to evaluate test results - see setupTrait() - * @param {Object} cardOptions Data for the card display, title, template, etc. - * @param {Object} rerenderMessage The message to be updated (used if editing the chat card) - */ - async traitTest({ testData, cardOptions }, options = {}) { - if (game.user.targets.size) { - cardOptions.title += ` - ${game.i18n.localize("Opposed")}`, - cardOptions.isOpposedTest = true - } - testData = await game.wfrp4e.dice.rollDices(testData, cardOptions); - let result = game.wfrp4e.dice.rollTest(testData); - result.postFunction = "traitTest"; - try { - // If the specification of a trait is a number, it's probably damage. (Animosity (Elves) - not a number specification: no damage) - if (!isNaN(testData.extra.trait.data.specification.value) || testData.extra.trait.data.rollable.rollCharacteristic == "ws" || testData.extra.trait.data.rollabble.rollCharacteristic == "bs") // (Bite 7 - is a number specification, do damage) - { - testData.extra.damage = Number(result.SL) // Start damage off with SL - testData.extra.damage += Number(testData.extra.trait.data.specification.value) || 0 - - if (testData.extra.trait.data.rollable.bonusCharacteristic) // Add the bonus characteristic (probably strength) - testData.extra.damage += Number(this.data.data.characteristics[testData.extra.trait.data.rollable.bonusCharacteristic].bonus) || 0; - } - } - catch (error) { - ui.notifications.error(game.i18n.localize("CHAT.DamageError") + " " + error) - } // If something went wrong calculating damage, do nothing and still render the card - - if (testData.extra) - foundry.utils.mergeObject(result, testData.extra); - - try { - let contextAudio = await game.wfrp4e.audio.MatchContextAudio(game.wfrp4e.audio.FindContext(result)) - cardOptions.sound = contextAudio.file || cardOptions.sound - } - catch - { } - Hooks.call("wfrp4e:rollTraitTest", result, cardOptions) - - if (!options.suppressMessage) - game.wfrp4e.dice.renderRollCard(cardOptions, result, options.rerenderMessage).then(msg => { - game.wfrp4e.opposed.handleOpposedTarget(msg) // Send to handleOpposed to determine opposed status, if any. - }) - return {result, cardOptions}; - } - - - /* --------------------------------------------------------------------------------------------------------- */ - /* --------------------------------- Preparation & Calculation Functions ----------------------------------- */ - /* --------------------------------------------------------------------------------------------------------- */ - /** - * Preparation function takes raw item data and processes it with actor data, typically using the calculate - * functions to do so. For example, A weapon passed into prepareWeaponCombat will turn the weapon's damage - * from "SB + 4" to the actual damage value by using the actor's strength bonus. See the specific functions - * below for more details on what exactly is processed. These functions are used when rolling a test - * (determining a weapon's base damage) or setting up the actor sheet to be displayed (displaying the damage - * in the combat tab). - * - /* --------------------------------------------------------------------------------------------------------- */ - - /** - * Prepares actor data for display and other features. - * - * prepare() is the principal function behind taking every aspect of an actor and processing them - * for display (getData() - see ActorSheetWfrp4e) and other needs. This is where all items (call to - * prepareItems()) are prepared and organized, then used to calculate different Actor features, - * such as the Size trait influencing wounds and token size, or how talents might affect damage. - * In many areas here, these talents/traits that affect some calculation are updated only if a - * difference is detected to avoid infinite loops, I would like an alternative but I'm not sure - * where to go instead. - * - * NOTE: THIS FUNCTION IS NOT TO BE CONFUSED WITH prepareData(). That function is called upon updating - * an actor. This function is called whenever the sheet is rendered. - */ - prepare() { - let preparedData = foundry.utils.duplicate(this.data) - // Call prepareItems first to organize and process OwnedItems - foundry.utils.mergeObject(preparedData, this.prepareItems()) - - // Add speciality functions for each Actor type - if (preparedData.type == "character") - this.prepareCharacter(preparedData) - - if (preparedData.type == "npc") - this.prepareNPC(preparedData) - - if (preparedData.type == "creature") - this.prepareCreature(preparedData) - - return preparedData; - } - - - /** - * Iterates through the Owned Items, processes them and organizes them into containers. - * - * This behemoth of a function goes through all Owned Items, separating them into individual arrays - * that the html templates use. Before adding them into the array, they are typically processed with - * the actor data, which can either be a large function itself (see prepareWeaponCombat) or not, such - * as career items which have minimal processing. These items, as well as some auxiliary data (e.g. - * encumbrance, AP) are bundled into an return object - * - */ - prepareItems() { - let actorData = foundry.utils.duplicate(this.data) - // These containers are for the various different tabs - const careers = []; - const basicSkills = []; - const advancedOrGroupedSkills = []; - const talents = []; - const traits = []; - const weapons = []; - const armour = []; - const injuries = []; - const grimoire = []; - const petty = []; - const blessings = []; - const miracles = []; - const psychology = []; - const mutations = []; - const diseases = []; - const criticals = []; - const extendedTests = []; - let penalties = { - [game.i18n.localize("Armour")]: { - value: "" - }, - [game.i18n.localize("Injury")]: { - value: "" - }, - [game.i18n.localize("Mutation")]: { - value: "" - }, - [game.i18n.localize("Criticals")]: { - value: "" - }, - }; - - const AP = { - head: { - value: 0, - layers: [], - label : game.i18n.localize("Head"), - show: true, - }, - body: { - value: 0, - layers: [], - label : game.i18n.localize("Body"), - show: true - }, - rArm: { - value: 0, - layers: [], - label : game.i18n.localize("Left Arm"), - show: true - }, - lArm: { - value: 0, - layers: [], - label : game.i18n.localize("Right Arm"), - show: true - }, - rLeg: { - value: 0, - layers: [], - label : game.i18n.localize("Right Leg"), - show: true - - }, - lLeg: { - value: 0, - layers: [], - label : game.i18n.localize("Left Leg"), - show: true - }, - shield: 0 - } - - for(let loc in AP) - { - if (loc == "shield") - continue - let row = game.wfrp4e.tables[actorData.data.details.hitLocationTable.value].rows.find(r => r.result == loc) - if (row) - AP[loc].label = game.i18n.localize(row.description) - else - AP[loc].show = false; - } - - - // Inventory object is for the Trappings tab - each sub object is for an individual inventory section - const inventory = { - weapons: { - label: game.i18n.localize("WFRP4E.TrappingType.Weapon"), // Label - what is displayed in the inventory section header - items: [], // Array of items in the section - toggle: true, // Is there a toggle in the section? (Equipped, worn, etc.) - toggleName: game.i18n.localize("Equipped"), // What is the name of the toggle in the header - show: false, // Should this section be shown (if an item exists in this list, it is set to true) - dataType: "weapon" // What type of FVTT Item is in this section (used by the + button to add an item of this type) - }, - armor: { - label: game.i18n.localize("WFRP4E.TrappingType.Armour"), - items: [], - toggle: true, - toggleName: game.i18n.localize("Worn"), - show: false, - dataType: "armour" - }, - ammunition: { - label: game.i18n.localize("WFRP4E.TrappingType.Ammunition"), - items: [], - show: false, - dataType: "ammunition" - }, - clothingAccessories: { - label: game.i18n.localize("WFRP4E.TrappingType.ClothingAccessories"), - items: [], - toggle: true, - toggleName: game.i18n.localize("Worn"), - show: false, - dataType: "trapping" - }, - booksAndDocuments: { - label: game.i18n.localize("WFRP4E.TrappingType.BooksDocuments"), - items: [], - show: false, - dataType: "trapping" - }, - toolsAndKits: { - label: game.i18n.localize("WFRP4E.TrappingType.ToolsKits"), - items: [], - show: false, - dataType: "trapping" - }, - foodAndDrink: { - label: game.i18n.localize("WFRP4E.TrappingType.FoodDrink"), - items: [], - show: false, - dataType: "trapping" - }, - drugsPoisonsHerbsDraughts: { - label: game.i18n.localize("WFRP4E.TrappingType.DrugsPoisonsHerbsDraughts"), - items: [], - show: false, - dataType: "trapping" - }, - misc: { - label: game.i18n.localize("WFRP4E.TrappingType.Misc"), - items: [], - show: true, - dataType: "trapping" - } - }; - - // Money and ingredients are not in inventory object because they need more customization - note in actor-inventory.html that they do not exist in the main inventory loop - const ingredients = { - label: game.i18n.localize("WFRP4E.TrappingType.Ingredient"), - items: [], - show: false, - dataType: "trapping" - }; - const money = { - coins: [], - total: 0, // Total coinage value - show: true - }; - const containers = { - items: [], - show: false - }; - const inContainers = []; // inContainers is the temporary storage for items within a container - - - let totalEnc = 0; // Total encumbrance of items - let hasSpells = false; // if the actor has atleast a single spell - used to display magic tab - let hasPrayers = false; // if the actor has atleast a single prayer - used to display religion tab - let showOffhand = true; // Show offhand checkboxes if no offhand equipped - let defensiveCounter = 0; // Counter for weapons with the defensive quality - - actorData.items = actorData.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)) - - // Iterate through items, allocating to containers - // Items that need more intense processing are sent to a specialized function (see preparation functions below) - // Physical items are also placed into containers instead of the inventory object if their 'location' is not 0 - // A location of 0 means not in a container, otherwise, the location corresponds to the id of the container the item is in - for (let i of actorData.items) { - try { - i.img = i.img || DEFAULT_TOKEN; - - // *********** TALENTS *********** - if (i.type === "talent") { - this.prepareTalent(i, talents); - } - - // *********** Skills *********** - else if (i.type === "skill") { - this.prepareSkill(i); - if (i.data.grouped.value == "isSpec" || i.data.advanced.value == "adv") - advancedOrGroupedSkills.push(i) - else - basicSkills.push(i); - } - - // *********** Ammunition *********** - else if (i.type === "ammunition") { - i.encumbrance = (i.data.encumbrance.value * i.data.quantity.value).toFixed(2); - if (i.data.location.value == 0) { - inventory.ammunition.items.push(i); - inventory.ammunition.show = true - totalEnc += Number(i.encumbrance); - } - else { - inContainers.push(i); - } - } - - // *********** Weapons *********** - // Weapons are "processed" at the end for efficency - else if (i.type === "weapon") { - i.encumbrance = Math.floor(i.data.encumbrance.value * i.data.quantity.value); - if (i.data.location.value == 0) { - i.toggleValue = i.data.equipped || false; - inventory.weapons.items.push(i); - inventory.weapons.show = true; - totalEnc += i.encumbrance; - } - else { - inContainers.push(i); - } - } - - // *********** Armour *********** - // Armour is prepared only if it is worn, otherwise, it is just pushed to inventory and encumbrance is calculated - else if (i.type === "armour") { - i.encumbrance = Math.floor(i.data.encumbrance.value * i.data.quantity.value); - if (i.data.location.value == 0) { - i.toggleValue = i.data.worn.value || false; - if (i.data.worn.value) { - i.encumbrance = i.encumbrance - 1; - i.encumbrance = i.encumbrance < 0 ? 0 : i.encumbrance; - } - inventory.armor.items.push(i); - inventory.armor.show = true; - totalEnc += i.encumbrance; - } - else { - inContainers.push(i); - } - - if (i.data.worn.value) - armour.push(this.prepareArmorCombat(i, AP)); - - } - // *********** Injuries *********** - else if (i.type == "injury") { - injuries.push(i); - penalties[game.i18n.localize("Injury")].value += i.data.penalty.value; - } - - // *********** Criticals *********** - else if (i.type == "critical") { - criticals.push(i); - penalties[game.i18n.localize("Criticals")].value += i.data.modifier.value; - } - - // *********** Containers *********** - // Items within containers are organized at the end - else if (i.type === "container") { - i.encumbrance = i.data.encumbrance.value; - - if (i.data.location.value == 0) { - if (i.data.worn.value) { - i.encumbrance = i.encumbrance - 1; - i.encumbrance = i.encumbrance < 0 ? 0 : i.encumbrance; - } - totalEnc += i.encumbrance; - } - else { - inContainers.push(i); - } - containers.items.push(i); - containers.show = true; - } - - // *********** Trappings *********** - // Trappings have several sub-categories, most notably Ingredients - // The trappings tab does not have a "Trappings" section, but sections for each type of trapping instead - else if (i.type === "trapping") { - i.encumbrance = i.data.encumbrance.value * i.data.quantity.value; - if (i.data.location.value == 0) { - // Push ingredients to a speciality array for futher customization in the trappings tab - if (i.data.trappingType.value == "ingredient") { - ingredients.items.push(i) - } - // The trapping will fall into one of these if statements and set the array accordingly - else if (i.data.trappingType.value == "clothingAccessories") { - i.toggleValue = i.data.worn || false; - inventory[i.data.trappingType.value].items.push(i); - inventory[i.data.trappingType.value].show = true; - if (i.data.worn) { - i.encumbrance = i.encumbrance - 1; // Since some trappings are worn, they need special treatment - i.encumbrance = i.encumbrance < 0 ? 0 : i.encumbrance; // This if statement is specific to worn clothing Trappings - } - } - else if (i.data.trappingType.value == "tradeTools") { - inventory["toolsAndKits"].items.push(i) // I decided not to separate "Trade Tools" and "Tools and Kits" - inventory["toolsAndKits"].show = true; // Instead, merging them both into "Tools and Kits" - } - else if (i.data.trappingType.value) { - inventory[i.data.trappingType.value].items.push(i); // Generic - add anything else to their appropriate array - inventory[i.data.trappingType.value].show = true; - } - else { - inventory.misc.items.push(i); // If somehow it didn't fall into the other categories (it should) - inventory.misc.show = true; // Just push it to miscellaneous - } - totalEnc += i.encumbrance; - } - else { - inContainers.push(i); - } - } - - // *********** Spells *********** - // See this.prepareSpellOrPrayer() for how these items are processed - else if (i.type === "spell") { - hasSpells = true; - if (i.data.lore.value == "petty") - petty.push(this.prepareSpellOrPrayer(i)); - else - grimoire.push(this.prepareSpellOrPrayer(i)); - } - // *********** Prayers *********** - // See this.prepareSpellOrPrayer() for how these items are processed - else if (i.type === "prayer") { - hasPrayers = true; - if (i.data.type.value == "blessing") - blessings.push(this.prepareSpellOrPrayer(i)); - else - miracles.push(this.prepareSpellOrPrayer(i)); - } - - // *********** Careers *********** - else if (i.type === "career") { - careers.push(i); - } - - // *********** Trait *********** - // Display Traits as Trait-Name (Specification) - // Such as Animosity (Elves) - else if (i.type === "trait") { - if (i.data.specification.value) { - if (i.data.rollable.bonusCharacteristic) // Bonus characteristic adds to the specification (Weapon +X includes SB for example) - { - i.data.specification.value = parseInt(i.data.specification.value) || 0 - i.data.specification.value += actorData.data.characteristics[i.data.rollable.bonusCharacteristic].bonus; - } - i.name = i.name + " (" + i.data.specification.value + ")"; - } - traits.push(i); - } - - // *********** Psychologies *********** - else if (i.type === "psychology") { - psychology.push(i); - } - - // *********** Diseases *********** - // .roll is the roll result. If it doesn't exist, show the formula instead - else if (i.type === "disease") { - i.data.incubation.roll = i.data.incubation.roll || i.data.incubation.value; - i.data.duration.roll = i.data.duration.roll || i.data.duration.value; - diseases.push(i); - } - - // *********** Mutations *********** - // Some mutations have modifiers - see the penalties section below - else if (i.type === "mutation") { - mutations.push(i); - if (i.data.modifiesSkills.value) - penalties[game.i18n.localize("Mutation")].value += i.data.modifier.value; - } - - // *********** Money *********** - // Keep a running total of the coin value the actor has outside of containers - else if (i.type === "money") { - i.encumbrance = (i.data.encumbrance.value * i.data.quantity.value).toFixed(2); - if (i.data.location.value == 0) { - money.coins.push(i); - totalEnc += Number(i.encumbrance); - } - else { - inContainers.push(i); - } - money.total += i.data.quantity.value * i.data.coinValue.value; - } - - else if (i.type === "extendedTest") { - i.pct = 0; - if (i.data.SL.target > 0) - i.pct = i.data.SL.current / i.data.SL.target * 100 - if (i.pct > 100) - i.pct = 100 - if (i.pct < 0) - i.pct = 0; - - extendedTests.push(i); - } - } - catch (error) { - console.error("Something went wrong with preparing item " + i.name + ": " + error) - ui.notifications.error("Something went wrong with preparing item " + i.name + ": " + error) - // ui.notifications.error("Deleting " + i.name); - // this.deleteEmbeddedEntity("OwnedItem", i._id); - } - } // END ITEM SORTING - - // Prepare weapons for combat after items passthrough for efficiency - weapons need to know the ammo possessed, so instead of iterating through - // all items to find, iterate through the inventory.ammo array we just made - let totalShieldDamage = 0; // Used for damage tooltip - let eqpPoints = 0 // Weapon equipment value, only 2 one handed weapons or 1 two handed weapon - for (let wep of inventory.weapons.items) { - // We're only preparing equipped items here - this is for displaying weapons in the combat tab after all - if (wep.data.equipped) { - if (getProperty(wep, "data.offhand.value")) - showOffhand = false; // Don't show offhand checkboxes if a weapon is offhanded - // Process weapon taking into account actor data, skills, and ammo - weapons.push(this.prepareWeaponCombat(wep, inventory.ammo, basicSkills.concat(advancedOrGroupedSkills))); - // Add shield AP to AP object - let shieldProperty = wep.properties.qualities.find(q => q.toLowerCase().includes(game.i18n.localize("PROPERTY.Shield").toLowerCase())) - if (shieldProperty) { - let shieldDamage = wep.data.APdamage || 0; - AP.shield += (parseInt(shieldProperty.split(" ")[1]) - shieldDamage); - totalShieldDamage += shieldDamage; - } - // Keep a running total of defensive weapons equipped - if (wep.properties.qualities.find(q => q.toLowerCase().includes(game.i18n.localize("PROPERTY.Defensive").toLowerCase()))) { - defensiveCounter++; - } - eqpPoints += wep.data.twohanded.value ? 2 : 1 - } - } - - this.data.flags.eqpPoints = eqpPoints - - - // If you have no spells, just put all ingredients in the miscellaneous section, otherwise, setup the ingredients to be available - if (grimoire.length > 0 && ingredients.items.length > 0) { - ingredients.show = true; - // For each spell, set available ingredients to ingredients that have been assigned to that spell - for (let s of grimoire) - s.data.ingredients = ingredients.items.filter(i => i.data.spellIngredient.value == s._id && i.data.quantity.value > 0) - } - else - inventory.misc.items = inventory.misc.items.concat(ingredients.items); - - - - // ******************************** Container Setup *********************************** - - // containerMissing is an array of items whose container does not exist (needed for when a container is deleted) - var containerMissing = inContainers.filter(i => !containers.items.find(c => c._id == i.data.location.value)); - for (var itemNoContainer of containerMissing) // Reset all items without container references (items that were removed from a contanier) - itemNoContainer.data.location.value = 0; - - // If there were missing containers, reset the items that are orphaned - if (containerMissing.length) - this.updateEmbeddedEntity("OwnedItem", containerMissing) - - for (var cont of containers.items) // For each container - { - // All items referencing (inside) that container - var itemsInside = inContainers.filter(i => i.data.location.value == cont._id); - itemsInside.map(function (item) { // Add category of item to be displayed - if (item.type == "trapping") - item.type = game.wfrp4e.config.trappingCategories[item.data.trappingType.value]; - else - item.type = game.wfrp4e.config.trappingCategories[item.type]; - }) - cont["carrying"] = itemsInside.filter(i => i.type != "Container"); // cont.carrying -> items the container is carrying - cont["packsInside"] = itemsInside.filter(i => i.type == "Container"); // cont.packsInside -> containers the container is carrying - cont["holding"] = itemsInside.reduce(function (prev, cur) { // cont.holding -> total encumbrance the container is holding - return Number(prev) + Number(cur.encumbrance); - }, 0); - cont.holding = Math.floor(cont.holding) - } - - containers.items = containers.items.filter(c => c.data.location.value == 0); // Do not show containers inside other containers as top level (a location value of 0 means not inside a container) - - - // ******************************** Penalties Setup *********************************** - - // Penalties box setup - // If too much text, divide the penalties into groups - let penaltyOverflow = false; - penalties[game.i18n.localize("Armour")].value += this.calculateArmorPenalties(armour); - if ((penalties[game.i18n.localize("Armour")].value + penalties[game.i18n.localize("Mutation")].value + penalties[game.i18n.localize("Injury")].value + penalties[game.i18n.localize("Criticals")].value).length > 50) // ~50 characters is when the text box overflows - { // When that happens, break it up into categories - penaltyOverflow = true; - for (let penaltyType in penalties) { - if (penalties[penaltyType].value) - penalties[penaltyType].show = true; - else - penalties[penaltyType].show = false; // Don't show categories without any penalties - } - } - - // Penalties flag is teh string that shows when the actor's turn in combat starts - let penaltiesFlag = penalties[game.i18n.localize("Armour")].value + " " + penalties[game.i18n.localize("Mutation")].value + " " + penalties[game.i18n.localize("Injury")].value + " " + penalties[game.i18n.localize("Criticals")].value + " " + this.data.data.status.penalties.value - penaltiesFlag = penaltiesFlag.trim(); - - // This is for the penalty string in flags, for combat turn message - if (this.data.flags.modifier != penaltiesFlag) - this.update({ "flags.modifier": penaltiesFlag }) - - // Add armor trait to AP object - let armorTrait = traits.find(t => t.name.toLowerCase().includes(game.i18n.localize("NAME.Armour").toLowerCase())) - if (armorTrait && (!this.data.data.excludedTraits || !this.data.data.excludedTraits.includes(armorTrait._id))) { - for (let loc in AP) { - try { - let traitDamage = 0; - if (armorTrait.APdamage) - traitDamage = armorTrait.APdamage[loc] || 0; - if (loc != "shield") - AP[loc].value += (parseInt(armorTrait.data.specification.value) || 0) - traitDamage; - } - catch {//ignore armor traits with invalid values - } - } - } - - // keep defensive counter in flags to use for test auto fill (see setupWeapon()) - this.data.flags.defensive = defensiveCounter; - - // Encumbrance is initially calculated in prepareItems() - this area augments it based on talents - if (actorData.flags.autoCalcEnc) { - let strongBackTalent = talents.find(t => t.name.toLowerCase() == game.i18n.localize("NAME.StrongBack").toLowerCase()) - let sturdyTalent = talents.find(t => t.name.toLowerCase() == game.i18n.localize("NAME.Sturdy").toLowerCase()) - - if (strongBackTalent) - actorData.data.status.encumbrance.max += strongBackTalent.data.advances.value; - if (sturdyTalent) - actorData.data.status.encumbrance.max += sturdyTalent.data.advances.value * 2; - } - - - // enc used for encumbrance bar in trappings tab - let enc; - totalEnc = Math.floor(totalEnc); - enc = { - max: actorData.data.status.encumbrance.max, - value: Math.round(totalEnc * 10) / 10, - }; - // percentage of the bar filled - enc.pct = Math.min(enc.value * 100 / enc.max, 100); - enc.state = enc.value / enc.max; // state is how many times over you are max encumbrance - if (enc.state > 3) { - enc["maxEncumbered"] = true - enc.penalty = game.wfrp4e.config.encumbrancePenalties["maxEncumbered"]; - } - else if (enc.state > 2) { - enc["veryEncumbered"] = true - enc.penalty = game.wfrp4e.config.encumbrancePenalties["veryEncumbered"]; - } - else if (enc.state > 1) { - enc["encumbered"] = true - enc.penalty = game.wfrp4e.config.encumbrancePenalties["encumbered"]; - } - else - enc["notEncumbered"] = true; - - // Return all processed objects - return { - inventory, - containers, - basicSkills: basicSkills.sort(game.wfrp4e.utility.nameSorter), - advancedOrGroupedSkills: advancedOrGroupedSkills.sort(game.wfrp4e.utility.nameSorter), - talents, - traits, - weapons, - diseases, - mutations, - armour, - penalties, - penaltyOverflow, - AP, - injuries, - grimoire, - petty, - careers: careers.reverse(), - blessings, - miracles, - money, - psychology, - criticals, - criticalCount: criticals.length, - encumbrance: enc, - ingredients, - totalShieldDamage, - extendedTests, - hasSpells, - hasPrayers, - showOffhand - } - } - - /** - * Prepares a skill Item. - * - * Preparation of a skill is simply determining the `total` value, which is the base characteristic + advances. - * - * @param {Object} skill 'skill' type Item - * @return {Object} skill Processed skill, with total value calculated - */ - prepareSkill(skill) { - let actorData = this.data - skill.data.characteristic.num = actorData.data.characteristics[skill.data.characteristic.value].value; - if (skill.data.modifier) - { - if (skill.data.modifier.value > 0) - skill.modified = "positive"; - else if (skill.data.modifier.value < 0) - skill.modified = "negative" - } - skill.data.characteristic.abrev = game.wfrp4e.config.characteristicsAbbrev[skill.data.characteristic.value]; - skill.data.cost = game.wfrp4e.utility._calculateAdvCost(skill.data.advances.value, "skill", skill.data.advances.costModifier) - return skill - } - - /** - * - * Prepares a talent Item. - * - * Prepares a talent with actor data and other talents. Two different ways to prepare a talent: - * - * 1. If a talent with the same name is already prepared, don't prepare this talent and instead - * add to the advancements of the existing talent. - * - * 2. If the talent does not exist yet, turn its "Max" value into "numMax", in other words, turn - * "Max: Initiative Bonus" into an actual number value. - * - * @param {Object} talent 'talent' type Item. - * @param {Array} talentList List of talents prepared so far. Prepared talent is pushed here instead of returning. - */ - prepareTalent(talent, talentList) { - let actorData = this.data - - // Find an existing prepared talent with the same name - let existingTalent = talentList.find(t => t.name == talent.name) - if (existingTalent) // If it exists - { - if (!existingTalent.numMax) // If for some reason, it does not have a numMax, assign it one - talent["numMax"] = actorData.data.characteristics[talent.data.max.value].bonus; - // Add an advancement to the existing talent - existingTalent.data.advances.value++; - existingTalent.cost = (existingTalent.data.advances.value + 1) * 100 - } - else // If a talent of the same name does not exist - { - switch (talent.data.max.value) // Turn its max value into "numMax", which is an actual numeric value - { - case '1': - talent["numMax"] = 1; - break; - - case '2': - talent["numMax"] = 2; - break; - - case 'none': - talent["numMax"] = "-"; - break; - - default: - talent["numMax"] = actorData.data.characteristics[talent.data.max.value].bonus; - } - talent.cost = 200; - talentList.push(talent); // Add the prepared talent to the talent list - } - } - - /** - * Prepares a weapon Item. - * - * Prepares a weapon using actor data, ammunition, properties, and flags. The weapon's raw - * data is turned into more user friendly / meaningful data with either config values or - * calculations. Also turns all qualities/flaws into a more structured object. - * - * @param {Object} weapon 'weapon' type Item - * @param {Array} ammoList array of 'ammo' type Items - * @param {Array} skills array of 'skill' type Items - * - * @return {Object} weapon processed weapon - */ - prepareWeaponCombat(weapon, ammoList, skills) { - let actorData = this.data - - if (!skills) // If a skill list isn't provided, filter all items to find skills - skills = actorData.items.filter(i => i.type == "skill"); - - weapon.attackType = game.wfrp4e.config.groupToType[weapon.data.weaponGroup.value] - weapon.data.reach.value = game.wfrp4e.config.weaponReaches[weapon.data.reach.value]; - weapon.data.weaponGroup.value = game.wfrp4e.config.weaponGroups[weapon.data.weaponGroup.value] || "basic"; - - // Attach the available skills to use to the weapon. - weapon.skillToUse = skills.find(x => x.name.toLowerCase().includes(`(${weapon.data.weaponGroup.value.toLowerCase()})`)) - - // prepareQualitiesFlaws turns the comma separated qualities/flaws string into a string array - // Does not include qualities if no skill could be found above - weapon["properties"] = game.wfrp4e.utility._prepareQualitiesFlaws(weapon, !!weapon.skillToUse); - - // Special flail rule - if no skill could be found, add the Dangerous property - if (weapon.data.weaponGroup.value == game.i18n.localize("SPEC.Flail") && !weapon.skillToUse && !weapon.properties.includes(game.i18n.localize("PROPERTY.Dangerous"))) - weapon.properties.push(game.i18n.localize("PROPERTY.Dangerous")); - - // Turn range into a numeric value (important for ranges including SB, see the function for details) - weapon.data.range.value = this.calculateRangeOrDamage(weapon.data.range.value); - - // Melee Damage calculation - if (weapon.attackType == "melee") { - weapon["meleeWeaponType"] = true; - // Turn melee damage formula into a numeric value (SB + 4 into a number) Melee damage increase flag comes from Strike Mighty Blow talent - weapon.data.damage.value = this.calculateRangeOrDamage(weapon.data.damage.value) + (actorData.flags.meleeDamageIncrease || 0); - - // Very poor wording, but if the weapon has suffered damage (weaponDamage), subtract that amount from meleeValue (melee damage the weapon deals) - if (weapon.data.weaponDamage) - weapon.data.damage.value -= weapon.data.weaponDamage - else - weapon.data["weaponDamage"] = 0; - } - // Ranged Damage calculation - else { - weapon["rangedWeaponType"] = true; - - // Turn ranged damage formula into numeric value, same as melee Ranged damage increase flag comes from Accurate Shot - weapon.data.damage.value = this.calculateRangeOrDamage(weapon.data.damage.value) + (actorData.flags.rangedDamageIncrease || 0) - // Very poor wording, but if the weapon has suffered damage (weaponDamage), subtract that amount from rangedValue (ranged damage the weapon deals) - if (weapon.data.weaponDamage) - weapon.data.damage.value -= weapon.data.weaponDamage - else - weapon.data["weaponDamage"] = 0; - } - - // If the weapon uses ammo... - if (weapon.data.ammunitionGroup.value != "none") { - weapon["ammo"] = []; - // If a list of ammo has been provided, filter it by ammo that is compatible with the weapon type - if (ammoList) { - weapon.ammo = ammoList.filter(a => a.data.ammunitionType.value == weapon.data.ammunitionGroup.value) - } - else // If no ammo has been provided, filter through all items and find ammo that is compaptible - for (let a of actorData.items) - if (a.type == "ammunition" && a.data.ammunitionType.value == weapon.data.ammunitionGroup.value) // If is ammo and the correct type of ammo - weapon.ammo.push(a); - - // Send to prepareWeaponWithAmmo for further calculation (Damage/range modifications based on ammo) - this.prepareWeaponWithAmmo(weapon); - } - // If throwing or explosive weapon, its ammo is its own quantity - else if (weapon.data.weaponGroup.value == game.i18n.localize("SPEC.Throwing") || weapon.data.weaponGroup.value == game.i18n.localize("SPEC.Explosives")) { - weapon.data.ammunitionGroup.value = ""; - } - // If entangling, it has no ammo - else if (weapon.data.weaponGroup.value == game.i18n.localize("SPEC.Entangling")) { - weapon.data.ammunitionGroup.value = ""; - } - // Separate qualities and flaws into their own arrays: weapon.properties.qualities/flaws - weapon.properties = game.wfrp4e.utility._separateQualitiesFlaws(weapon.properties); - - if (weapon.properties.spec) - { - for(let prop of weapon.properties.spec) - { - let spec - if (prop == game.i18n.localize("Special")) - weapon.properties.special = weapon.data.special.value; - if (prop == game.i18n.localize("Special Ammo")) - weapon.properties.specialammo = weapon.ammo.find(a => a._id == weapon.data.currentAmmo.value).data.special.value - } - - } - return weapon; - } - - /** - * Prepares an armour Item. - * - * Takes a an armour item, along with a persistent AP object to process the armour - * into a useable format. Adding AP values and qualities to the AP object to be used - * in display and opposed tests. - * - * @param {Object} armor 'armour' type item - * @param {Object} AP Object consisting of numeric AP value for each location and a layer array to represent each armour layer - * @return {Object} armor processed armor item - */ - prepareArmorCombat(armor, AP) { - // Turn comma separated qualites/flaws into a more structured 'properties.qualities/flaws` string array - armor.properties = game.wfrp4e.utility._separateQualitiesFlaws(game.wfrp4e.utility._prepareQualitiesFlaws(armor)); - - // Iterate through armor locations covered - for (let apLoc in armor.data.currentAP) { - // -1 is what all newly created armor's currentAP is initialized to, so if -1: currentAP = maxAP (undamaged) - if (armor.data.currentAP[apLoc] == -1) { - armor.data.currentAP[apLoc] = armor.data.maxAP[apLoc]; - } - } - // If the armor protects a certain location, add the AP value of the armor to the AP object's location value - // Then pass it to addLayer to parse out important information about the armor layer, namely qualities/flaws - if (armor.data.maxAP.head > 0) { - armor["protectsHead"] = true; - AP.head.value += armor.data.currentAP.head; - game.wfrp4e.utility.addLayer(AP, armor, "head") - } - if (armor.data.maxAP.body > 0) { - armor["protectsBody"] = true; - AP.body.value += armor.data.currentAP.body; - game.wfrp4e.utility.addLayer(AP, armor, "body") - } - if (armor.data.maxAP.lArm > 0) { - armor["protectslArm"] = true; - AP.lArm.value += armor.data.currentAP.lArm; - game.wfrp4e.utility.addLayer(AP, armor, "lArm") - } - if (armor.data.maxAP.rArm > 0) { - armor["protectsrArm"] = true; - AP.rArm.value += armor.data.currentAP.rArm; - game.wfrp4e.utility.addLayer(AP, armor, "rArm") - } - if (armor.data.maxAP.lLeg > 0) { - armor["protectslLeg"] = true; - AP.lLeg.value += armor.data.currentAP.lLeg; - game.wfrp4e.utility.addLayer(AP, armor, "lLeg") - } - if (armor.data.maxAP.rLeg > 0) { - armor["protectsrLeg"] = true - AP.rLeg.value += armor.data.currentAP.rLeg; - game.wfrp4e.utility.addLayer(AP, armor, "rLeg") - } - return armor; - } - - - /** - * Augments a prepared weapon based on its equipped ammo. - * - * Ammo can provide bonuses or penalties to the weapon using it, this function - * takes a weapon, finds its current ammo, and applies those modifiers to the - * weapon stats. For instance, if ammo that halves weapon range is equipped, - * this is where it modifies the range of the weapon - * - * @param {Object} weapon A *prepared* weapon item - * @return {Object} weapon Augmented weapon item - */ - prepareWeaponWithAmmo(weapon) { - // Find the current ammo equipped to the weapon, if none, return - let ammo = weapon.ammo.find(a => a._id == weapon.data.currentAmmo.value); - if (!ammo) - return; - - let ammoProperties = game.wfrp4e.utility._prepareQualitiesFlaws(ammo); - // If ammo properties include a "special" value, rename the property as "Special Ammo" to not overlap - // with the weapon's "Special" property - let specialPropInd = ammoProperties.indexOf(ammoProperties.find(p => p && p.toLowerCase() == game.i18n.localize("Special").toLowerCase())); - if (specialPropInd != -1) - ammoProperties[specialPropInd] = game.i18n.localize("Special Ammo") - - let ammoRange = ammo.data.range.value || "0"; - let ammoDamage = ammo.data.damage.value || "0"; - - // If range modification was handwritten, process it - if (ammoRange.toLowerCase() == "as weapon") { } - // Do nothing to weapon's range - else if (ammoRange.toLowerCase() == "half weapon") - weapon.data.range.value /= 2; - else if (ammoRange.toLowerCase() == "third weapon") - weapon.data.range.value /= 3; - else if (ammoRange.toLowerCase() == "quarter weapon") - weapon.data.range.value /= 4; - else if (ammoRange.toLowerCase() == "twice weapon") - weapon.data.range.value *= 2; - else // If the range modification is a formula (supports +X -X /X *X) - { - try // Works for + and - - { - ammoRange = eval(ammoRange); - weapon.data.range.value = Math.floor(eval(weapon.data.range.value + ammoRange)); - } - catch // if *X and /X - { // eval (50 + "/5") = eval(50/5) = 10 - weapon.data.range.value = Math.floor(eval(weapon.data.range.value + ammoRange)); - } - } - - try // Works for + and - - { - ammoDamage = eval(ammoDamage); - weapon.data.damage.value = Math.floor(eval(weapon.data.damage.value + ammoDamage)); - } - catch // if *X and /X - { // eval (5 + "*2") = eval(5*2) = 10 - weapon.data.damage.value = Math.floor(eval(weapon.data.damage.value + ammoDamage)); // Eval throws exception for "/2" for example. - } - - // The following code finds qualities or flaws of the ammo that add to the weapon's qualities - // Example: Blast +1 should turn a weapon's Blast 4 into Blast 5 - ammoProperties = ammoProperties.filter(p => p != undefined); - let propertyChange = ammoProperties.filter(p => p.includes("+") || p.includes("-")); // Properties that increase or decrease another (Blast +1, Blast -1) - - // Normal properties (Impale, Penetrating) from ammo that need to be added to the equipped weapon - let propertiesToAdd = ammoProperties.filter(p => !(p.includes("+") || p.includes("-"))); - - - for (let change of propertyChange) { - // Using the example of "Blast +1" to a weapon with "Blast 3" - let index = change.indexOf(" "); - let property = change.substring(0, index).trim(); // "Blast" - let value = change.substring(index, change.length); // "+1" - - if (weapon.properties.find(p => p.includes(property))) // Find the "Blast" quality in the main weapon - { - let basePropertyIndex = weapon.properties.findIndex(p => p.includes(property)) - let baseValue = weapon.properties[basePropertyIndex].split(" ")[1]; // Find the Blast value of the weapon (3) - let newValue = eval(baseValue + value) // Assign the new value of Blast 4 - - weapon.properties[basePropertyIndex] = `${property} ${newValue}`; // Replace old Blast - } - else // If the weapon does not have the Blast quality to begin with - { - propertiesToAdd.push(property + " " + Number(value)); // Add blast as a new quality (Blast 1) - } - } - // Add the new Blast property to the rest of the qualities the ammo adds to the weapon - weapon.properties = weapon.properties.concat(propertiesToAdd); - } - - /** - * Prepares a 'spell' or 'prayer' Item type. - * - * Calculates many aspects of spells/prayers defined by characteristics - range, duration, damage, aoe, etc. - * See the calculation function used for specific on how it processes these attributes. - * - * @param {Object} item 'spell' or 'prayer' Item - * @return {Object} item Processed spell/prayer - */ - prepareSpellOrPrayer(item) { - // Turns targets and duration into a number - (e.g. Willpower Bonus allies -> 4 allies, Willpower Bonus Rounds -> 4 rounds, Willpower Yards -> 46 yards) - item['target'] = this.calculateSpellAttributes(item.data.target.value, item.data.target.aoe); - item['duration'] = this.calculateSpellAttributes(item.data.duration.value); - item['range'] = this.calculateSpellAttributes(item.data.range.value); - - item.overcasts = { - available: 0, - range: undefined, - duration: undefined, - target: undefined, - } - - if (parseInt(item.target)) { - item.overcasts.target = { - label: "Target", - count: 0, - AoE: false, - initial: parseInt(item.target) || item.target, - current: parseInt(item.target) || item.target, - unit: "" - } - } - else if (item.target.includes("AoE")) { - let aoeValue = item.target.substring(item.target.indexOf("(") + 1, item.target.length - 1) - item.overcasts.target = { - label: "AoE", - count: 0, - AoE: true, - initial: parseInt(aoeValue) || aoeValue, - current: parseInt(aoeValue) || aoeValue, - unit: aoeValue.split(" ")[1] - } - } - if (parseInt(item.duration)) { - item.overcasts.duration = { - label: "Duration", - count: 0, - initial: parseInt(item.duration) || item.duration, - current: parseInt(item.duration) || item.duration, - unit: item.duration.split(" ")[1] - } - } - if (parseInt(item.range)) { - item.overcasts.range = { - label: "Range", - count: 0, - initial: parseInt(item.range) || aoeValue, - current: parseInt(item.range) || aoeValue, - unit: item.range.split(" ")[1] - } - } - - // Add the + to the duration if it's extendable - if (item.data.duration.extendable) - item.duration += "+"; - - // Calculate the damage different if it's a Magic Misile spell versus a prayer - if (item.type == "spell") - item['damage'] = this.calculateSpellDamage(item.data.damage.value, item.data.magicMissile.value); - else - item['damage'] = this.calculateSpellDamage(item.data.damage.value, false); - - // If it's a spell, augment the description (see _spellDescription() and CN based on memorization) - if (item.type == "spell") { - item.data.description.value = game.wfrp4e.utility._spellDescription(item); - if (!item.data.memorized.value) - item.data.cn.value *= 2; - } - - return item; - } - - - /** - * Turns a formula into a processed string for display - * - * Turns a spell attribute such as "Willpower Bonus Rounds" into a more user friendly, processed value - * such as "4 Rounds". If the aoe is checked, it wraps the result in AoE (Result). - * - * @param {String} formula Formula to process - "Willpower Bonus Rounds" - * @param {boolean} aoe Whether or not it's calculating AoE (changes string return) - * @returns {String} formula processed formula - */ - calculateSpellAttributes(formula, aoe=false) - { - console.log("Compute FR") - let actorData = this.data - formula = formula.toLowerCase(); - - // Do not process these special values - if (formula != game.i18n.localize("You").toLowerCase() && formula != game.i18n.localize("Special").toLowerCase() && formula != game.i18n.localize("Instant").toLowerCase()) - { - // Specific case, to avoid wrong matching with "Force" - if (formula.includes("force mentale")) - { - // Determine if it's looking for the bonus or the value - if (formula.includes('bonus')) { - formula = formula.replace( "bonus de force mentale", actorData.data.characteristics["wp"].bonus); - formula = formula.replace( "force mentale bonus", actorData.data.characteristics["wp"].bonus); - } - else - formula = formula.replace("force mentale", actorData.data.characteristics["wp"].value); - } - if (formula.includes("yard") ) - formula = formula.replace('yard', "mètre" ); - if (formula.includes("yds") ) - formula = formula.replace('yds', "m." ); - // Iterate through remaining characteristics - for(let ch in actorData.data.characteristics) - { - // If formula includes characteristic name - //console.log("Testing :", ch, WFRP4E.characteristics[ch].toLowerCase()); - if (formula.includes(game.wfrp4e.config.characteristics[ch].toLowerCase())) - { - // Determine if it's looking for the bonus or the value - if (formula.includes('bonus')) { - formula = formula.replace("bonus de " + game.wfrp4e.config.characteristics[ch].toLowerCase(), actorData.data.characteristics[ch].bonus); - formula = formula.replace(game.wfrp4e.config.characteristics[ch].toLowerCase() + " bonus", actorData.data.characteristics[ch].bonus); - } - else - formula = formula.replace(game.wfrp4e.config.characteristics[ch].toLowerCase(), actorData.data.characteristics[ch].value); - } - } - } - - // If AoE - wrap with AoE ( ) - if (aoe) - formula = "AoE (" + formula.capitalize() + ")"; - - //console.log("calculateSpellAttributes -> " + formula ); - return formula.capitalize(); - } - - /** - * Turns a formula into a processed string for display - * - * Processes damage formula based - same as calculateSpellAttributes, but with additional - * consideration to whether its a magic missile or not - * - * @param {String} formula Formula to process - "Willpower Bonus + 4" - * @param {boolean} isMagicMissile Whether or not it's a magic missile - used in calculating additional damage - * @returns {String} Processed formula - */ - calculateSpellDamage(formula, isMagicMissile) - { - console.log("Compute FR") - let actorData = this.data - formula = formula.toLowerCase(); - - if (isMagicMissile) // If it's a magic missile, damage includes willpower bonus - { - formula += "+ " + actorData.data.characteristics["wp"].bonus - } - - // Specific case, to avoid wrong matching with "Force" - if (formula.includes("force mentale")) - { - // Determine if it's looking for the bonus or the value - if (formula.includes('bonus')) { - formula = formula.replace( "bonus de force mentale", actorData.data.characteristics["wp"].bonus); - formula = formula.replace( "force mentale bonus", actorData.data.characteristics["wp"].bonus); - } else - formula = formula.replace("force mentale", actorData.data.characteristics["wp"].value); - } - - // Iterate through characteristics - for(let ch in actorData.data.characteristics) - { - // If formula includes characteristic name - while (formula.includes(actorData.data.characteristics[ch].label.toLowerCase())) - { - // Determine if it's looking for the bonus or the value - if (formula.includes('bonus')) { - formula = formula.replace("bonus de " + game.wfrp4e.config.characteristics[ch].toLowerCase(), actorData.data.characteristics[ch].bonus); - formula = formula.replace(game.wfrp4e.config.characteristics[ch].toLowerCase() + " bonus", actorData.data.characteristics[ch].bonus); - } - else - formula = formula.replace(game.wfrp4e.config.characteristics[ch].toLowerCase(), actorData.data.characteristics[ch].value); - } - } - - //console.log("calculateSpellDamage -> " + formula ); - return eval(formula); - } - - /** - * Construct armor penalty string based on armors equipped. - * - * For each armor, compile penalties and concatenate them into one string. - * Does not stack armor *type* penalties. - * - * @param {Array} armorList array of processed armor items - * @return {string} Penalty string - */ - calculateArmorPenalties(armorList) { - let armorPenaltiesString = ""; - - // Armor type penalties do not stack, only apply if you wear any of that type - let wearingMail = false; - let wearingPlate = false; - - for (let a of armorList) { - // For each armor, apply its specific penalty value, as well as marking down whether - // it qualifies for armor type penalties (wearingMail/Plate) - armorPenaltiesString += a.data.penalty.value + " "; - if (a.data.armorType.value == "mail") - wearingMail = true; - if (a.data.armorType.value == "plate") - wearingPlate = true; - } - - // Apply armor type penalties at the end - if (wearingMail || wearingPlate) { - let stealthPenaltyValue = 0; - if (wearingMail) - stealthPenaltyValue += -10; - if (wearingPlate) - stealthPenaltyValue += -10; - // Add the penalties together to reduce redundancy - armorPenaltiesString += (stealthPenaltyValue + ` ${game.i18n.localize("NAME.Stealth")}`); - } - return armorPenaltiesString; - } - - /** - * Calculates a weapon's range or damage formula. - * - * Takes a weapon formula for Damage or Range (SB + 4 or SBx3) and converts to a numeric value. - * - * @param {String} formula formula to be processed (SBx3 => 9). - * - * @return {Number} Numeric formula evaluation - */ - calculateRangeOrDamage(formula) - { - console.log("FR function calculateRangeOrDamage !", formula); - let actorData = this.data - try - { - formula = formula.toLowerCase(); - // Iterate through characteristics - for(let ch in actorData.data.characteristics) - { - // Determine if the formula includes the characteristic's abbreviation + B (SB, WPB, etc.) - if (formula.includes(ch.concat('b'))) - { - // Replace that abbreviation with the Bonus value - formula = formula.replace(ch.concat('b'), actorData.data.characteristics[ch].bonus.toString()); - } - } - if (formula.includes("yard") ) - formula = formula.replace('yard', "mètre" ); - if (formula.includes("yds") ) - formula = formula.replace('yds', "m." ); - // To evaluate multiplication, replace x with * - formula = formula.replace('x', '*'); - - return eval(formula); - } - catch - { - return formula - } - } - - /** - * Adds all missing basic skills to the Actor. - * - * This function will add all mising basic skills, used when an Actor is created (see create()) - * as well as from the right click menu from the Actor directory. - * - */ - async addBasicSkills() { - let allItems = foundry.utils.duplicate(this.data.items) - let ownedBasicSkills = allItems.filter(i => i.type == "skill" && i.data.advanced.value == "bsc"); - let allBasicSkills = await game.wfrp4e.utility.allBasicSkills() - - // Filter allBasicSkills with ownedBasicSkills, resulting in all the missing skills - let skillsToAdd = allBasicSkills.filter(s => !ownedBasicSkills.find(ownedSkill => ownedSkill.name == s.name)) - - // Add those missing basic skills - this.createEmbeddedEntity("OwnedItem", skillsToAdd); - } - - /** - * Calculates the wounds of an actor based on prepared items - * - * Once all the item preparation is done (prepareItems()), we have a list of traits/talents to use that will - * factor into Wonuds calculation. Namely: Hardy and Size traits. If we find these, they must be considered - * in Wound calculation. - * - * @returns {Number} Max wound value calculated - */ - _calculateWounds() { - let hardies = this.data.items.filter(t => (t.type == "trait" || t.type == "talent") && t.name.toLowerCase().includes(game.i18n.localize("NAME.Hardy").toLowerCase())) - let traits = this.data.items.filter(t => t.type == "trait") - - let tbMultiplier = hardies.length - - tbMultiplier += hardies.filter(h => h.type == "talent").reduce((extra, talent) => extra + talent.data.advances.value - 1, 0) // Add extra advances if some of the talents had multiple advances (rare, usually there are multiple talent items, not advances) - - - // Easy to reference bonuses - let sb = this.data.data.characteristics.s.bonus; - let tb = this.data.data.characteristics.t.bonus; - let wpb = this.data.data.characteristics.wp.bonus; - - if (this.data.flags.autoCalcCritW) - this.data.data.status.criticalWounds.max = tb; - - let wounds = this.data.data.status.wounds.max; - - if (this.data.flags.autoCalcWounds) { - // Construct trait means you use SB instead of WPB - if (traits.find(t => t.name.toLowerCase().includes(game.i18n.localize("NAME.Construct").toLowerCase()) || traits.find(t => t.name.toLowerCase().includes(game.i18n.localize("NAME.Mindless").toLowerCase())))) - wpb = sb; - switch (this.data.data.details.size.value) // Use the size to get the correct formula (size determined in prepare()) - { - case "tiny": - wounds = 1 + tb * tbMultiplier; - break; - - case "ltl": - wounds = tb + tb * tbMultiplier; - break; - - case "sml": - wounds = 2 * tb + wpb + tb * tbMultiplier; - break; - - case "avg": - wounds = sb + 2 * tb + wpb + tb * tbMultiplier; - break; - - case "lrg": - wounds = 2 * (sb + 2 * tb + wpb + tb * tbMultiplier); - break; - - case "enor": - wounds = 4 * (sb + 2 * tb + wpb + tb * tbMultiplier); - break; - - case "mnst": - wounds = 8 * (sb + 2 * tb + wpb + tb * tbMultiplier); - break; - } - } - - let swarmTrait = traits.find(t => t.name.toLowerCase().includes(game.i18n.localize("NAME.Swarm").toLowerCase())) - if (swarmTrait) - wounds *= 5; - - - return wounds - } - - /** - * Apply damage to an actor, taking into account armor, size, and weapons. - * - * applyDamage() is typically called at the end of an oppposed tests, where you can - * right click the chat message and apply damage. This function goes through the - * process of calculating and reducing damage if needede based on armor, toughness, - * size, armor qualities/flaws, and weapon qualities/flaws - * - * @param {Object} victim id of actor taking damage - * @param {Object} opposedData Test results, all the information needed to calculate damage - * @param {var} damageType enum for what the damage ignores, see config.js - */ - static applyDamage(victim, opposeData, damageType = game.wfrp4e.config.DAMAGE_TYPE.NORMAL) { - if (!opposeData.damage) - return `Error: ${game.i18n.localize("CHAT.DamageAppliedError")}` - // If no damage value, don't attempt anything - if (!opposeData.damage.value) - return game.i18n.localize("CHAT.DamageAppliedErrorTiring"); - - // Get actor/tokens for those in the opposed test - let actor = game.wfrp4e.utility.getSpeaker(victim); - let attacker = game.wfrp4e.utility.getSpeaker(opposeData.speakerAttack) - let soundContext = { item: {}, action: "hit" }; - - // Start wound loss at the damage value - let totalWoundLoss = opposeData.damage.value - let newWounds = actor.data.data.status.wounds.value; - let applyAP = (damageType == game.wfrp4e.config.DAMAGE_TYPE.IGNORE_TB || damageType == game.wfrp4e.config.DAMAGE_TYPE.NORMAL) - let applyTB = (damageType == game.wfrp4e.config.DAMAGE_TYPE.IGNORE_AP || damageType == game.wfrp4e.config.DAMAGE_TYPE.NORMAL) - let AP = {}; - - // Start message update string - let updateMsg = `${game.i18n.localize("CHAT.DamageApplied")}" - updateMsg = updateMsg.replace("@TOTAL", totalWoundLoss) - - // Update actor wound value - actor.update({ "data.status.wounds.value": newWounds }) - return updateMsg; - } - - - - /* --------------------------------------------------------------------------------------------------------- */ - /* -------------------------------------- Auto-Advancement Functions --------------------------------------- */ - /* --------------------------------------------------------------------------------------------------------- */ - /** - * These functions are primarily for NPCs and Creatures and their automatic advancement capabilities. - * - /* --------------------------------------------------------------------------------------------------------- */ - - - /** - * Advances an actor's skills based on their species and character creation rules - * - * Per character creation, 3 skills from your species list are advanced by 5, and 3 more are advanced by 3. - * This functions uses the Foundry Roll class to randomly select skills from the list (defined in config.js) - * and advance the first 3 selected by 5, and the second 3 selected by 3. This function uses the advanceSkill() - * helper defined below. - */ - async _advanceSpeciesSkills() { - let skillList - - // A species may not be entered in the actor, so use some error handling. - try { - skillList = game.wfrp4e.config.speciesSkills[this.data.data.details.species.value]; - if (!skillList) { - // findKey() will do an inverse lookup of the species key in the species object defined in config.js, and use that if - // user-entered species value does not work (which it probably will not) - skillList = game.wfrp4e.config.speciesSkills[game.wfrp4e.utility.findKey(this.data.data.details.species.value, game.wfrp4e.config.species)] - if (!skillList) { - throw game.i18n.localize("Error.SpeciesSkills") + " " + this.data.data.details.species.value; - } - } - } - catch (error) { - ui.notifications.info("Could not find species " + this.data.data.details.species.value) - console.log("wfrp4e | Could not find species " + this.data.data.details.species.value + ": " + error); - throw error - } - // The Roll class used to randomly select skills - let skillSelector = new Roll(`1d${skillList.length}- 1`); - skillSelector.roll().total; - - // Store selected skills - let skillsSelected = []; - while (skillsSelected.length < 6) { - skillSelector = skillSelector.reroll() - if (!skillsSelected.includes(skillSelector.total)) // Do not push duplicates - skillsSelected.push(skillSelector.total); - } - - // Advance the first 3 by 5, advance the second 3 by 3. - for (let skillIndex = 0; skillIndex < skillsSelected.length; skillIndex++) { - if (skillIndex <= 2) - await this._advanceSkill(skillList[skillsSelected[skillIndex]], 5) - else - await this._advanceSkill(skillList[skillsSelected[skillIndex]], 3) - } - } - - - /** - * Advances an actor's talents based on their species and character creation rules - * - * Character creation rules for talents state that you get all talents in your species, but there - * are a few where you must choose between two instead. See config.js for how the species talent - * object is set up for support in this. Basically species talents are an array of strings, however - * ones that offer a choice is formatted as "How does ${this.name} resist this corruption?`,
- buttons: {
- endurance: {
- label: game.i18n.localize("NAME.Endurance"),
- callback: () => {
- let skill = this.items.find(i => i.name == game.i18n.localize("NAME.Endurance") && i.type == "skill")
- if (skill) {
- this.setupSkill(skill.data, { corruption: strength }).then(setupData => this.basicTest(setupData))
- }
- else {
- this.setupCharacteristic("t", { corruption: strength }).then(setupData => this.basicTest(setupData))
- }
- }
- },
- cool: {
- label: game.i18n.localize("NAME.Cool"),
- callback: () => {
- let skill = this.items.find(i => i.name == game.i18n.localize("NAME.Cool") && i.type == "skill")
- if (skill) {
- this.setupSkill(skill.data, { corruption: strength }).then(setupData => this.basicTest(setupData))
- }
- else {
- this.setupCharacteristic("wp", { corruption: strength }).then(setupData => this.basicTest(setupData))
- }
- }
- }
-
- }
- }).render(true)
- }
-
- async handleCorruptionResult(testResult) {
- let strength = testResult.options.corruption;
- let failed = testResult.target < testResult.roll;
- let corruption = 0 // Corruption GAINED
- switch (strength) {
- case "minor":
- if (failed)
- corruption++;
- break;
-
- case "moderate":
- if (failed)
- corruption += 2
- else if (testResult.SL < 2)
- corruption += 1
- break;
-
- case "major":
- if (failed)
- corruption += 3
- else if (testResult.SL < 2)
- corruption += 2
- else if (testResult.SL < 4)
- corruption += 1
- break;
- }
- let newCorruption = Number(this.data.data.status.corruption.value) + corruption
- ChatMessage.create(game.wfrp4e.utility.chatDataSetup(`${this.name} gains ${corruption} Corruption.`, "gmroll", false))
- await this.update({ "data.status.corruption.value": newCorruption })
- if (corruption > 0)
- this.checkCorruption();
-
- }
-
- async checkCorruption() {
- if (this.data.data.status.corruption.value > this.data.data.status.corruption.max) {
- let skill = this.items.find(i => i.name == game.i18n.localize("NAME.Endurance") && i.type == "skill")
- if (skill) {
- this.setupSkill(skill.data, { mutate: true }).then(setupData => {
- this.basicTest(setupData)
- });
- }
- else {
- this.setupCharacteristic("t", { mutate: true }).then(setupData => {
- this.basicTest(setupData)
- });
- }
- }
- }
-
- async handleMutationResult(testResult) {
- let failed = testResult.target < testResult.roll;
-
- if (failed) {
- let wpb = this.data.data.characteristics.wp.bonus;
- let tableText = "Roll on a Corruption Table:
" + game.wfrp4e.config.corruptionTables.map(t => `@Table[${t}]
`).join("")
- ChatMessage.create(game.wfrp4e.utility.chatDataSetup(`
-
As corruption ravages your soul, the warping breath of Chaos whispers within, either fanning your flesh into a fresh, new form, or fracturing your psyche with exquisite knowledge it can never unlearn.
-${this.name} loses ${wpb} Corruption. -
${tableText}
`, - "gmroll", false)) - this.update({ "data.status.corruption.value": Number(this.data.data.status.corruption.value) - wpb }) - } - else - ChatMessage.create(game.wfrp4e.utility.chatDataSetup(`You have managed to hold off your corruption. For now.`, "gmroll", false)) - - } - - - async handleExtendedTest(testResult) { - let test = foundry.utils.duplicate(this.getEmbeddedEntity("OwnedItem", testResult.options.extended)); - - if(game.settings.get("wfrp4e", "extendedTests") && testResult.SL == 0) - testResult.SL = testResult.roll <= testResult.target ? 1 : -1 - - if (test.data.failingDecreases.value) - { - test.data.SL.current += Number(testResult.SL) - if (!test.data.negativePossible.value && test.data.SL.current < 0) - test.data.SL.current = 0; - } - else if(testResult.SL > 0) - test.data.SL.current += Number(testResult.SL) - - let displayString = `${test.name} ${test.data.SL.current} / ${test.data.SL.target} SL` - - if (test.data.SL.current >= test.data.SL.target) - { - if (test.data.completion.value == "reset") - test.data.SL.current = 0; - else if (test.data.completion.value == "remove") - { - this.deleteEmbeddedEntity("OwnedItem", test._id) - test = undefined - } - displayString = displayString.concat("