forked from public/foundryvtt-wh4-lang-fr-fr
		
	
		
			
				
	
	
		
			3613 lines
		
	
	
		
			152 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			3613 lines
		
	
	
		
			152 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * 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: `<p>${game.i18n.localize("ACTOR.BasicSkillsPrompt")}</p>`,
 | |
|         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(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 = 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)
 | |
|       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 = 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 = 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)
 | |
|       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 = duplicate(this.data)
 | |
|     // Call prepareItems first to organize and process OwnedItems
 | |
|     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 = 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 = 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 `<b>Error</b>: ${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 = `<b>${game.i18n.localize("CHAT.DamageApplied")}</b><span class = 'hide-option'>: @TOTAL`;
 | |
|     if (damageType != game.wfrp4e.config.DAMAGE_TYPE.IGNORE_ALL)
 | |
|       updateMsg += " ("
 | |
| 
 | |
|     let weaponProperties
 | |
|     // If armor at hitloc has impenetrable value or not
 | |
|     let impenetrable = false;
 | |
|     // If weapon is undamaging
 | |
|     let undamaging = false;
 | |
|     // If weapon has Hack
 | |
|     let hack = false;
 | |
|     // If weapon has Impale
 | |
|     let impale = false;
 | |
|     // If weapon has Penetrating
 | |
|     let penetrating = false;
 | |
| 
 | |
|     // if weapon has pummel - only used for audio
 | |
|     let pummel = false
 | |
| 
 | |
|     // Reduce damage by TB
 | |
|     if (applyTB) {
 | |
|       totalWoundLoss -= actor.data.data.characteristics.t.bonus
 | |
|       updateMsg += actor.data.data.characteristics.t.bonus + " TB"
 | |
|     }
 | |
| 
 | |
|     // If the actor has the Robust talent, reduce damage by times taken
 | |
|     totalWoundLoss -= actor.data.flags.robust || 0;
 | |
| 
 | |
|     if (actor.data.flags.robust)
 | |
|       updateMsg += ` + ${actor.data.flags.robust} Robust`
 | |
| 
 | |
|     if (applyAP) {
 | |
|       AP = actor.prepareItems().AP[opposeData.hitloc.value]
 | |
|       AP.ignored = 0;
 | |
|       if (opposeData.attackerTestResult.weapon) // If the attacker is using a weapon
 | |
|       {
 | |
|         // Determine its qualities/flaws to be used for damage calculation
 | |
|         weaponProperties = opposeData.attackerTestResult.weapon.properties;
 | |
|         penetrating = weaponProperties.qualities.includes(game.i18n.localize("PROPERTY.Penetrating"))
 | |
|         undamaging = weaponProperties.flaws.includes(game.i18n.localize("PROPERTY.Undamaging"))
 | |
|         hack = weaponProperties.qualities.includes(game.i18n.localize("PROPERTY.Hack"))
 | |
|         impale = weaponProperties.qualities.includes(game.i18n.localize("PROPERTY.Impale"))
 | |
|         pummel = weaponProperties.qualities.includes(game.i18n.localize("PROPERTY.Pummel"))
 | |
|       }
 | |
|       // see if armor flaws should be triggered
 | |
|       let ignorePartial = opposeData.attackerTestResult.roll % 2 == 0 || opposeData.attackerTestResult.extra.critical
 | |
|       let ignoreWeakpoints = opposeData.attackerTestResult.extra.critical && impale
 | |
| 
 | |
|       // Mitigate damage with armor one layer at a time
 | |
|       for (let layer of AP.layers) {
 | |
|         if (ignoreWeakpoints && layer.weakpoints) {
 | |
|           AP.ignored += layer.value
 | |
|         }
 | |
|         else if (ignorePartial && layer.partial) {
 | |
|           AP.ignored += layer.value;
 | |
|         }
 | |
|         else if (penetrating) // If penetrating - ignore 1 or all armor depending on material
 | |
|         {
 | |
|           AP.ignored += layer.metal ? 1 : layer.value
 | |
|         }
 | |
|         if (opposeData.attackerTestResult.roll % 2 != 0 && layer.impenetrable) {
 | |
|           impenetrable = true;
 | |
|           soundContext.outcome = "impenetrable"
 | |
|         }
 | |
| 
 | |
|         // Prioritize plate over chain over leather for sound
 | |
|         if (layer.value) {
 | |
|           if (layer.armourType == "plate")
 | |
|             soundContext.item.armourType = layer.armourType
 | |
|           else if (!soundContext.item.armourType || (soundContext.item.armourType && (soundContext.item.armourType.includes("leather")) && layer.armourType == "mail")) // set to chain if there isn't an armour type set yet, or the current armor type is leather
 | |
|             soundContext.item.armourType = layer.armourType
 | |
|           else if (!soundContext.item.armourType)
 | |
|             soundContext.item.armourType = "leather"
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // AP.used is the actual amount of AP considered
 | |
|       AP.used = AP.value - AP.ignored
 | |
|       AP.used = AP.used < 0 ? 0 : AP.used;           // AP minimum 0
 | |
|       AP.used = undamaging ? AP.used * 2 : AP.used;  // Double AP if undamaging
 | |
| 
 | |
|       // show the AP usage in the updated message
 | |
|       if (AP.ignored)
 | |
|         updateMsg += ` + ${AP.used}/${AP.value} ${game.i18n.localize("AP")}`
 | |
|       else
 | |
|         updateMsg += ` + ${AP.used} ${game.i18n.localize("AP")}`
 | |
| 
 | |
|       // If using a shield, add that AP as well
 | |
|       let shieldAP = 0;
 | |
|       if (opposeData.defenderTestResult.weapon) {
 | |
|         if (opposeData.defenderTestResult.weapon.properties.qualities.find(q => q.toLowerCase().includes(game.i18n.localize("PROPERTY.Shield").toLowerCase())))
 | |
|           shieldAP = Number(opposeData.defenderTestResult.weapon.properties.qualities.find(q => q.toLowerCase().includes(game.i18n.localize("PROPERTY.Shield").toLowerCase())).split(" ")[1]);
 | |
|       }
 | |
| 
 | |
|       if (shieldAP)
 | |
|         updateMsg += ` + ${shieldAP} ${game.i18n.localize("CHAT.DamageShield")})`
 | |
|       else
 | |
|         updateMsg += ")"
 | |
| 
 | |
|       // Reduce damage done by AP
 | |
|       totalWoundLoss -= (AP.used + shieldAP)
 | |
| 
 | |
|       // Minimum 1 wound if not undamaging
 | |
|       if (!undamaging)
 | |
|         totalWoundLoss = totalWoundLoss <= 0 ? 1 : totalWoundLoss
 | |
|       else
 | |
|         totalWoundLoss = totalWoundLoss <= 0 ? 0 : totalWoundLoss
 | |
| 
 | |
| 
 | |
|       try {
 | |
|         if (opposeData.attackerTestResult.weapon.attackType == "melee") {
 | |
|           if ((weaponProperties.qualities.concat(weaponProperties.flaws)).every(p => [game.i18n.localize("PROPERTY.Pummel"), game.i18n.localize("PROPERTY.Slow"), game.i18n.localize("PROPERTY.Damaging")].includes(p)))
 | |
|             soundContext.outcome = "warhammer" // special sound for warhammer :^)
 | |
|           else if (AP.used) {
 | |
|             soundContext.item.type = "armour"
 | |
|             if (applyAP && totalWoundLoss <= 1)
 | |
|               soundContext.outcome = "blocked"
 | |
|             else if (applyAP)
 | |
|               soundContext.outcome = "normal"
 | |
|             if (impenetrable)
 | |
|               soundContext.outcome = "impenetrable"
 | |
|             if (hack)
 | |
|               soundContext.outcome = "hack"
 | |
|           }
 | |
|           else {
 | |
|             soundContext.item.type = "hit"
 | |
|             soundContext.outcome = "normal"
 | |
|             if (impale || penetrating) {
 | |
|               soundContext.outcome = "normal_slash"
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       catch (e) { console.log("wfrp4e | Sound Context Error: " + e) } // Ignore sound errors
 | |
|     }
 | |
|     else updateMsg += ")"
 | |
| 
 | |
|     newWounds -= totalWoundLoss
 | |
| 
 | |
|     game.wfrp4e.audio.PlayContextAudio(soundContext)
 | |
| 
 | |
|     // If damage taken reduces wounds to 0, show Critical
 | |
|     if (newWounds <= 0 && !impenetrable) {
 | |
|       //WFRP_Audio.PlayContextAudio(opposeData.attackerTestResult.weapon, {"type": "hit", "equip": "crit"})
 | |
|       let critAmnt = game.settings.get("wfrp4e", "dangerousCritsMod")
 | |
|       if (game.settings.get("wfrp4e", "dangerousCrits") && critAmnt && (Math.abs(newWounds) - actor.data.data.characteristics.t.bonus) > 0) {
 | |
|         let critModifier = (Math.abs(newWounds) - actor.data.data.characteristics.t.bonus) * critAmnt;
 | |
|         updateMsg += `<br><a class ="table-click critical-roll" data-modifier=${critModifier} data-table = "crit${opposeData.hitloc.value}" ><i class='fas fa-list'></i> ${game.i18n.localize("Critical")} +${critModifier}</a>`
 | |
|       }
 | |
|       else if (Math.abs(newWounds) < actor.data.data.characteristics.t.bonus)
 | |
|         updateMsg += `<br><a class ="table-click critical-roll" data-modifier="-20" data-table = "crit${opposeData.hitloc.value}" ><i class='fas fa-list'></i> ${game.i18n.localize("Critical")} (-20)</a>`
 | |
|       else
 | |
|         updateMsg += `<br><a class ="table-click critical-roll" data-table = "crit${opposeData.hitloc.value}" ><i class='fas fa-list'></i> ${game.i18n.localize("Critical")}</a>`
 | |
|     }
 | |
|     else if (impenetrable)
 | |
|       updateMsg += `<br>${game.i18n.localize("PROPERTY.Impenetrable")} - ${game.i18n.localize("CHAT.CriticalsNullified")}`
 | |
| 
 | |
|     if (hack)
 | |
|       updateMsg += `<br>${game.i18n.localize("CHAT.DamageAP")} ${game.wfrp4e.config.locations[opposeData.hitloc.value]}`
 | |
| 
 | |
|     if (newWounds <= 0)
 | |
|       newWounds = 0; // Do not go below 0 wounds
 | |
| 
 | |
| 
 | |
|     updateMsg += "</span>"
 | |
|     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 "<talent1>, <talent2>", each talent being a choice. Finally,
 | |
|    * the last element of the talent list is a number denoting the number of random talents. This function uses
 | |
|    * the advanceTalent() helper defined below.
 | |
|    */
 | |
|   async _advanceSpeciesTalents() {
 | |
|     // A species may not be entered in the actor, so use some error handling.
 | |
|     let talentList
 | |
|     try {
 | |
|       talentList = game.wfrp4e.config.speciesTalents[this.data.data.details.species.value];
 | |
|       if (!talentList) {
 | |
|         // 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)
 | |
|         talentList = game.wfrp4e.config.speciesTalents[game.wfrp4e.utility.findKey(this.data.data.details.species.value, game.wfrp4e.config.species)]
 | |
|         if (!talentList)
 | |
|           throw game.i18n.localize("Error.SpeciesTalents") + " " + 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
 | |
|     }
 | |
|     let talentSelector;
 | |
|     for (let talent of talentList) {
 | |
|       if (!isNaN(talent)) // If is a number, roll on random talents
 | |
|       {
 | |
|         for (let i = 0; i < talent; i++) {
 | |
|           let result = game.wfrp4e.tables.rollTable("talents")
 | |
|           await this._advanceTalent(result.name);
 | |
|         }
 | |
|         continue
 | |
|       }
 | |
|       // If there is a comma, talent.split() will yield an array of length > 1
 | |
|       let talentOptions = talent.split(',').map(function (item) {
 | |
|         return item.trim();
 | |
|       });
 | |
| 
 | |
|       // Randomly choose a talent option and advance it.
 | |
|       if (talentOptions.length > 1) {
 | |
|         talentSelector = new Roll(`1d${talentOptions.length} - 1`)
 | |
|         await this._advanceTalent(talentOptions[talentSelector.roll().total])
 | |
|       }
 | |
|       else // If no option, simply advance the talent.
 | |
|       {
 | |
|         await this._advanceTalent(talent)
 | |
|       }
 | |
|     }
 | |
| 
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Adds (if needed) and advances a skill by the specified amount.
 | |
|    * 
 | |
|    * As the name suggests, this function advances any given skill, if 
 | |
|    * the actor does not currently have that skill, it will be added 
 | |
|    * from the compendium and advanced. Note that this function is neither
 | |
|    * used by manually advancing skills nor when clicking on advancement 
 | |
|    * indicators. This will simply add the advancement value with no
 | |
|    * other processing.
 | |
|    * 
 | |
|    * @param {String} skillName    Name of the skill to advance/add
 | |
|    * @param {Number} advances     Advances to add to the skill
 | |
|    */
 | |
|   async _advanceSkill(skillName, advances) {
 | |
|     // Look through items and determine if the actor has the skill
 | |
|     let existingSkill = this.data.items.find(i => i.name.trim() == skillName && i.type == "skill")
 | |
|     // If so, simply update the skill with the new advancement value. 
 | |
|     if (existingSkill) {
 | |
|       existingSkill = duplicate(existingSkill);
 | |
|       // If the existing skill has a greater amount of advances, use the greater value instead (make no change) - ??? Is this needed? I'm not sure why I did this. TODO: Evaluate.
 | |
|       existingSkill.data.advances.value = (existingSkill.data.advances.value < advances) ? advances : existingSkill.data.advances.value;
 | |
|       await this.updateEmbeddedEntity("OwnedItem", existingSkill);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // If the actor does not already own skill, search through compendium and add it
 | |
|     try {
 | |
|       // See findSkill() for a detailed explanation of how it works
 | |
|       // Advanced find function, returns the skill the user expects it to return, even with skills not included in the compendium (Lore (whatever))
 | |
|       let skillToAdd = await game.wfrp4e.utility.findSkill(skillName)
 | |
|       skillToAdd.data.data.advances.value = advances;
 | |
|       await this.createEmbeddedEntity("OwnedItem", skillToAdd.data);
 | |
|     }
 | |
|     catch (error) {
 | |
|       console.error("Something went wrong when adding skill " + skillName + ": " + error);
 | |
|       ui.notifications.error("Something went wrong when adding skill " + skillName + ": " + error);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Adds the given talent to the actor
 | |
|    * 
 | |
|    * In my implementation, adding a talent is the same as advancing a talent. See
 | |
|    * prepareTalent() and you'll see that the total number of any given talent is the
 | |
|    * advencement value.
 | |
|    * 
 | |
|    * @param {String} talentName     Name of the talent to add/advance.
 | |
|    */
 | |
|   async _advanceTalent(talentName) {
 | |
|     try {
 | |
|       // See findTalent() for a detailed explanation of how it works
 | |
|       // Advanced find function, returns the Talent the user expects it to return, even with Talents not included in the compendium (Etiquette (whatever))
 | |
|       let talent = await game.wfrp4e.utility.findTalent(talentName);
 | |
|       await this.createEmbeddedEntity("OwnedItem", talent.data);
 | |
|     }
 | |
|     catch (error) {
 | |
|       console.error("Something went wrong when adding talent " + talentName + ": " + error);
 | |
|       ui.notifications.error("Something went wrong when adding talent " + talentName + ": " + error);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Advance NPC based on given career
 | |
|    * 
 | |
|    * A specialized function used by NPC type Actors that triggers when you click on a 
 | |
|    * career to be "complete". This takes all the career data and uses it (and the helpers
 | |
|    * defined above) to advance the actor accordingly. It adds all skills (advanced to the 
 | |
|    * correct amount to be considered complete), advances all characteristics similarly, and 
 | |
|    * adds all talents.
 | |
|    * 
 | |
|    * Note: This adds *all* skills and talents, which is not necessary to be considered complete.
 | |
|    * However, I find deleting the ones you don't want to be much easier than trying to pick and 
 | |
|    * choose the ones you do want.
 | |
|    *
 | |
|    * @param {Object} careerData     Career type Item to be used for advancement.
 | |
|    * 
 | |
|    * TODO Refactor for embedded entity along with the helper functions
 | |
|    */
 | |
|   async _advanceNPC(careerData) {
 | |
|     let updateObj = {};
 | |
|     let advancesNeeded = careerData.level.value * 5; // Tier 1 needs 5, 2 needs 10, 3 needs 15, 4 needs 20 in all characteristics and skills
 | |
| 
 | |
|     // Update all necessary characteristics to the advancesNeeded
 | |
|     for (let advChar of careerData.characteristics)
 | |
|       if (this.data.data.characteristics[advChar].advances < 5 * careerData.level.value)
 | |
|         updateObj[`data.characteristics.${advChar}.advances`] = 5 * careerData.level.value;
 | |
| 
 | |
|     // Advance all skills in the career
 | |
|     for (let skill of careerData.skills)
 | |
|       await this._advanceSkill(skill, advancesNeeded);
 | |
| 
 | |
|     // Add all talents in the career
 | |
|     for (let talent of careerData.talents)
 | |
|       await this._advanceTalent(talent);
 | |
| 
 | |
|     this.update(updateObj);
 | |
|   }
 | |
| 
 | |
| 
 | |
|   _replaceData(formula) {
 | |
|     let dataRgx = new RegExp(/@([a-z.0-9]+)/gi);
 | |
|     return formula.replace(dataRgx, (match, term) => {
 | |
|       let value = getProperty(this.data, term);
 | |
|       return value ? String(value).trim() : "0";
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Use a fortune point from the actor to reroll or add sl to a roll
 | |
|    * @param {Object} message 
 | |
|    * @param {String} type (reroll, addSL)
 | |
|    */
 | |
|   useFortuneOnRoll(message, type) {
 | |
|     if (this.data.data.status.fortune.value > 0) {
 | |
|       message.data.flags.data.preData.roll = undefined;
 | |
|       let data = message.data.flags.data;
 | |
|       let html = `<h3 class="center"><b>${game.i18n.localize("FORTUNE.Use")}</b></h3>`;
 | |
|       //First we send a message to the chat
 | |
|       if (type == "reroll")
 | |
|         html += `${game.i18n.format("FORTUNE.UsageRerollText", { character: '<b>' + this.name + '</b>' })}<br>`;
 | |
|       else
 | |
|         html += `${game.i18n.format("FORTUNE.UsageAddSLText", { character: '<b>' + this.name + '</b>' })}<br>`;
 | |
| 
 | |
|       html += `<b>${game.i18n.localize("FORTUNE.PointsRemaining")} </b>${this.data.data.status.fortune.value - 1}`;
 | |
|       ChatMessage.create(game.wfrp4e.utility.chatDataSetup(html));
 | |
| 
 | |
|       let cardOptions = this.preparePostRollAction(message);
 | |
|       //Then we do the actual fortune action
 | |
|       if (type == "reroll") {
 | |
|         cardOptions.fortuneUsedReroll = true;
 | |
|         cardOptions.hasBeenCalculated = false;
 | |
|         cardOptions.calculatedMessage = [];
 | |
|         //It was an unopposed targeted test who failed
 | |
|         if (data.originalTargets && data.originalTargets.size > 0) {
 | |
|           game.user.targets = data.originalTargets;
 | |
|           //Foundry has a circular reference to the user in its targets set so we do it too
 | |
|           game.user.targets.user = game.user;
 | |
|         }
 | |
|         //It it is an ongoing opposed test, we transfer the list of the startMessages to update them
 | |
|         if (!data.defenderMessage && data.startMessagesList) {
 | |
|           cardOptions.startMessagesList = data.startMessagesList;
 | |
|         }
 | |
|         delete data.preData.roll;
 | |
|         delete data.preData.SL;
 | |
|         new ActorWfrp4e_fr(data.postData.actor)[`${data.postData.postFunction}`]({testData : data.preData, cardOptions});
 | |
| 
 | |
|         //We also set fortuneUsedAddSL to force the player to use it on the new roll
 | |
|         message.update({
 | |
|           "flags.data.fortuneUsedReroll": true,
 | |
|           "flags.data.fortuneUsedAddSL": true
 | |
|         });
 | |
| 
 | |
|       }
 | |
|       else //addSL
 | |
|       {
 | |
|         let newTestData = data.preData;
 | |
|         newTestData.SL = Math.trunc(data.postData.SL) + 1;
 | |
|         newTestData.slBonus = 0;
 | |
|         newTestData.successBonus = 0;
 | |
|         newTestData.roll = Math.trunc(data.postData.roll);
 | |
|         newTestData.hitloc = data.preData.hitloc;
 | |
| 
 | |
|         //We deselect the token, 
 | |
|         //2020-04-25 : Currently the foundry function is bugged so we do it ourself
 | |
|         //game.user.updateTokenTargets([]);
 | |
|         game.user.targets.forEach(t => t.setTarget(false, { user: game.user, releaseOthers: false, groupSelection: true }));
 | |
| 
 | |
|         cardOptions.fortuneUsedAddSL = true;
 | |
|         new ActorWfrp4e_fr(data.postData.actor)[`${data.postData.postFunction}`]({testData : newTestData, cardOptions}, {rerenderMessage : message});
 | |
|         message.update({
 | |
|           "flags.data.fortuneUsedAddSL": true
 | |
|         });
 | |
|       }
 | |
|       this.update({ "data.status.fortune.value": this.data.data.status.fortune.value - 1 });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Take a Dark Deal to reroll for +1 Corruption
 | |
|    * @param {Object} message 
 | |
|    */
 | |
|   useDarkDeal(message) {
 | |
|     let html = `<h3 class="center"><b>${game.i18n.localize("DARKDEAL.Use")}</b></h3>`;
 | |
|     html += `${game.i18n.format("DARKDEAL.UsageText", { character: '<b>' + this.name + '</b>' })}<br>`;
 | |
|     let corruption = Math.trunc(this.data.data.status.corruption.value) + 1;
 | |
|     html += `<b>${game.i18n.localize("Corruption")}: </b>${corruption}/${this.data.data.status.corruption.max}`;
 | |
|     ChatMessage.create(game.wfrp4e.utility.chatDataSetup(html));
 | |
|     this.update({ "data.status.corruption.value": corruption }).then(() => {
 | |
|       this.checkCorruption();
 | |
|     });
 | |
| 
 | |
|     message.data.flags.data.preData.roll = undefined;
 | |
|     let cardOptions = this.preparePostRollAction(message);
 | |
|     let data = message.data.flags.data;
 | |
|     cardOptions.fortuneUsedReroll = data.fortuneUsedReroll;
 | |
|     cardOptions.fortuneUsedAddSL = data.fortuneUsedAddSL;
 | |
|     cardOptions.hasBeenCalculated = false;
 | |
|     cardOptions.calculatedMessage = [];
 | |
| 
 | |
|     //It was an unopposed targeted test who failed
 | |
|     if (data.originalTargets && data.originalTargets.size > 0) {
 | |
|       game.user.targets = data.originalTargets;
 | |
|       //Foundry has a circular reference to the user in its targets set so we do it too
 | |
|       game.user.targets.user = game.user;
 | |
|     }
 | |
|     //It it is an ongoing opposed test, we transfer the list of the startMessages to update them
 | |
|     if (!data.defenderMessage && data.startMessagesList) {
 | |
|       cardOptions.startMessagesList = data.startMessagesList;
 | |
|     }
 | |
|     delete message.data.flags.data.preData.roll;
 | |
|     delete message.data.flags.data.preData.SL;
 | |
|     new ActorWfrp4e_fr(data.postData.actor)[`${data.postData.postFunction}`]({testData : data.preData, cardOptions});
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * This helper can be used to prepare cardOptions to reroll/edit a test card
 | |
|    * It uses the informations of the roll located in the message entry
 | |
|    * from game.messages
 | |
|    * @param {Object} message 
 | |
|    * @returns {Object} cardOptions
 | |
|    */
 | |
|   preparePostRollAction(message) {
 | |
|     //recreate the initial (virgin) cardOptions object
 | |
|     //add a flag for reroll limit
 | |
|     let data = message.data.flags.data;
 | |
|     let cardOptions = {
 | |
|       flags: { img: message.data.flags.img },
 | |
|       rollMode: data.rollMode,
 | |
|       sound: message.data.sound,
 | |
|       speaker: message.data.speaker,
 | |
|       template: data.template,
 | |
|       title: data.title.replace(` - ${game.i18n.localize("Opposed")}`, ""),
 | |
|       user: message.data.user
 | |
|     };
 | |
|     if (data.attackerMessage)
 | |
|       cardOptions.attackerMessage = data.attackerMessage;
 | |
|     if (data.defenderMessage)
 | |
|       cardOptions.defenderMessage = data.defenderMessage;
 | |
|     if (data.unopposedStartMessage)
 | |
|       cardOptions.unopposedStartMessage = data.unopposedStartMessage;
 | |
|     return cardOptions;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   async corruptionDialog(strength) {
 | |
|     new Dialog({
 | |
|       title: "Corrupting Influence",
 | |
|       content: `<p>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(`<b>${this.name}</b> 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:<br>" + game.wfrp4e.config.corruptionTables.map(t => `@Table[${t}]<br>`).join("")
 | |
|       ChatMessage.create(game.wfrp4e.utility.chatDataSetup(`
 | |
|       <h3>Dissolution of Body and Mind</h3> 
 | |
|       <p>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.</p>
 | |
|       <p><b>${this.name}</b> loses ${wpb} Corruption.
 | |
|       <p>${tableText}</p>`,
 | |
|         "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 = 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("<br>" + "<b>Completed</b>")
 | |
|     }
 | |
| 
 | |
|     testResult.other.push(displayString)
 | |
| 
 | |
|     if (test)
 | |
|       this.updateEmbeddedEntity("OwnedItem", test);
 | |
|   }
 | |
| 
 | |
| 
 | |
| 
 | |
| } |