2020-11-06 17:51:31 +01:00
/ * *
* 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 ,
}
2020-11-07 00:42:04 +01:00
let basicSkills = await game . wfrp4e . utility . allBasicSkills ( ) || [ ] ;
let moneyItems = await game . wfrp4e . utility . allMoneyItems ( ) || [ ] ;
2020-11-06 17:51:31 +01:00
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 )
2020-11-07 00:42:04 +01:00
ch . cost = game . wfrp4e . utility . _calculateAdvCost ( ch . advances , "characteristic" )
2020-11-06 17:51:31 +01:00
}
}
/ * *
* 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
2020-11-07 00:42:04 +01:00
data . data . details . size . value = game . wfrp4e . utility . findKey ( size , game . wfrp4e . config . actorSizes ) || "avg"
2020-11-06 17:51:31 +01:00
// 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 ) {
2020-11-06 21:44:22 +01:00
//let tokenSize = WFRP4E.tokenSizes[data.data.details.size.value]
let tokenSize = game . wfrp4e . config . tokenSizes [ data . data . details . size . value ]
2020-11-06 17:51:31 +01:00
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 ;
}
}
/* --------------------------------------------------------------------------------------------------------- */
/ * S e t t i n g u p R o l l s
/ *
/ * A l l " s e t u p _ _ _ _ _ _ " f u n c t i o n s g a t h e r t h e d a t a n e e d e d t o r o l l a c e r t a i n t e s t . T h e s e a r e i n 3 m a i n o b j e c t s .
/ * T h e s e 3 o b j e c t s a r e t h e n g i v e n t o D i c e W F R P . s e t u p D i a l o g ( ) t o s h o w t h e d i a l o g , s e e t h a t f u n c t i o n f o r i t s u s a g e .
/ *
/ * T h e 3 M a i n o b j e c t s :
/ * t e s t D a t a - D a t a a s s o c i a t e d w i t h m o d i f i c a t i o n s t o r o l l i n g t h e t e s t i t s e l f , o r r e s u l t s o f t h e t e s t .
/ * E x a m p l e s o f t h i s a r e w h e t h e r h i t l o c a t i o n s a r e f o u n d , W e a p o n q u a l i t i e s t h a t m a y c a u s e
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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 ,
2020-11-06 21:44:22 +01:00
characteristicList : game . wfrp4e . config . characteristics ,
2020-11-06 17:51:31 +01:00
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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.
2024-05-31 12:13:20 +02:00
let wep = this . prepareWeaponCombat ( foundry . utils . duplicate ( weapon ) ) ;
2020-11-06 17:51:31 +01:00
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
2024-05-31 12:13:20 +02:00
testData . extra . ammo = foundry . utils . duplicate ( this . getEmbeddedEntity ( "OwnedItem" , weapon . data . currentAmmo . value ) )
2020-11-06 17:51:31 +01:00
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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 {
2020-11-06 21:44:22 +01:00
defaultSelection = channellSkills . indexOf ( channellSkills . find ( x => x . name . includes ( game . wfrp4e . config . magicWind [ spellLore ] ) ) ) ;
2020-11-06 17:51:31 +01:00
}
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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 ;
2020-11-06 21:44:22 +01:00
let title = game . wfrp4e . config . characteristics [ trait . data . rollable . rollCharacteristic ] + ` ${ game . i18n . localize ( "Test" ) } - ` + trait . name ;
2020-11-06 17:51:31 +01:00
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 ,
2020-11-06 21:44:22 +01:00
characteristicList : game . wfrp4e . config . characteristics ,
2020-11-06 17:51:31 +01:00
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 ( ) ) ;
2020-11-06 21:44:22 +01:00
testData . testDifficulty = game . wfrp4e . config . difficultyModifiers [ html . find ( '[name="testDifficulty"]' ) . val ( ) ] ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
return game . wfrp4e . dice . setupDialog ( {
2020-11-06 17:51:31 +01:00
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 = { } ) {
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollTest ( testData ) ;
2020-11-06 17:51:31 +01:00
result . postFunction = "basicTest" ;
if ( testData . extra )
2024-05-31 12:13:20 +02:00
foundry . utils . mergeObject ( result , testData . extra ) ;
2020-11-06 17:51:31 +01:00
if ( result . options . corruption ) {
this . handleCorruptionResult ( result ) ;
}
if ( result . options . mutate ) {
this . handleMutationResult ( result )
}
if ( result . options . extended )
{
this . handleExtendedTest ( result )
}
try {
2020-11-07 00:42:04 +01:00
let contextAudio = await game . wfrp4e . audio . MatchContextAudio ( game . wfrp4e . audio . FindContext ( result ) )
2020-11-06 17:51:31 +01:00
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 )
2020-11-07 00:42:04 +01:00
game . wfrp4e . dice . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
game . wfrp4e . opposed . handleOpposedTarget ( msg ) // Send to handleOpposed to determine opposed status, if any.
2020-11-06 17:51:31 +01:00
} )
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 = { } ) {
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollTest ( testData ) ;
2020-11-06 17:51:31 +01:00
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 ( ' ' )
2020-11-07 00:42:04 +01:00
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)
2020-11-06 17:51:31 +01:00
dieAmount = Number ( dieAmount ) * status [ 1 ] ; // Multilpy that first letter by your standing (Brass 4 = 8d10 pennies)
let moneyEarned ;
2020-11-07 00:42:04 +01:00
if ( game . wfrp4e . utility . findKey ( status [ 0 ] , game . wfrp4e . config . statusTiers ) != "g" ) // Don't roll for gold, just use standing value
2020-11-06 17:51:31 +01:00
{
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 ;
2020-11-07 00:42:04 +01:00
switch ( game . wfrp4e . utility . findKey ( status [ 0 ] , game . wfrp4e . config . statusTiers ) ) {
2020-11-06 17:51:31 +01:00
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 ;
2020-11-07 00:42:04 +01:00
switch ( game . wfrp4e . utility . findKey ( status [ 0 ] , game . wfrp4e . config . statusTiers ) ) {
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
result . moneyEarned = moneyEarned + game . wfrp4e . utility . findKey ( status [ 0 ] , game . wfrp4e . config . statusTiers ) ;
2020-11-06 17:51:31 +01:00
if ( ! options . suppressMessage )
2020-11-07 00:42:04 +01:00
game . wfrp4e . dice . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
game . wfrp4e . opposed . handleOpposedTarget ( msg )
2020-11-06 17:51:31 +01:00
} )
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
}
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollWeaponTest ( testData ) ;
2020-11-06 17:51:31 +01:00
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 {
2020-11-07 00:42:04 +01:00
let contextAudio = await game . wfrp4e . audio . MatchContextAudio ( game . wfrp4e . audio . FindContext ( result ) )
2020-11-06 17:51:31 +01:00
cardOptions . sound = contextAudio . file || cardOptions . sound
}
catch
{ }
Hooks . call ( "wfrp4e:rollWeaponTest" , result , cardOptions )
if ( ! options . suppressMessage )
2020-11-07 00:42:04 +01:00
game . wfrp4e . dice . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
game . wfrp4e . opposed . handleOpposedTarget ( msg ) // Send to handleOpposed to determine opposed status, if any.
2020-11-06 17:51:31 +01:00
} )
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
}
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollCastTest ( testData ) ;
2020-11-06 17:51:31 +01:00
result . postFunction = "castTest" ;
// Find ingredient being used, if any
2024-05-31 12:13:20 +02:00
let ing = foundry . utils . duplicate ( this . getEmbeddedEntity ( "OwnedItem" , testData . extra . spell . data . currentIng . value ) )
2020-11-06 17:51:31 +01:00
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 {
2020-11-07 00:42:04 +01:00
let contextAudio = await game . wfrp4e . audio . MatchContextAudio ( game . wfrp4e . audio . FindContext ( result ) )
2020-11-06 17:51:31 +01:00
cardOptions . sound = contextAudio . file || cardOptions . sound
}
catch
{ }
Hooks . call ( "wfrp4e:rollCastTest" , result , cardOptions )
// Update spell to reflect SL from channelling resetting to 0
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . getSpeaker ( cardOptions . speaker ) . updateEmbeddedEntity ( "OwnedItem" , { _id : testData . extra . spell . _id , 'data.cn.SL' : 0 } ) ;
2020-11-06 17:51:31 +01:00
if ( ! options . suppressMessage )
2020-11-07 00:44:43 +01:00
game . wfrp4e . dice . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
2020-11-07 00:42:04 +01:00
game . wfrp4e . opposed . handleOpposedTarget ( msg ) // Send to handleOpposed to determine opposed status, if any.
2020-11-06 17:51:31 +01:00
} )
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
}
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollChannellTest ( testData , game . wfrp4e . utility . getSpeaker ( cardOptions . speaker ) ) ;
2020-11-06 17:51:31 +01:00
result . postFunction = "channelTest" ;
// Find ingredient being used, if any
2024-05-31 12:13:20 +02:00
let ing = foundry . utils . duplicate ( this . getEmbeddedEntity ( "OwnedItem" , testData . extra . spell . data . currentIng . value ) )
2020-11-06 17:51:31 +01:00
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 {
2020-11-07 00:42:04 +01:00
let contextAudio = await game . wfrp4e . audio . MatchContextAudio ( game . wfrp4e . audio . FindContext ( result ) )
2020-11-06 17:51:31 +01:00
cardOptions . sound = contextAudio . file || cardOptions . sound
}
catch
{ }
Hooks . call ( "wfrp4e:rollChannelTest" , result , cardOptions )
if ( ! options . suppressMessage )
DiceWFRP . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
2020-11-07 00:42:04 +01:00
game . wfrp4e . opposed . handleOpposedTarget ( msg ) // Send to handleOpposed to determine opposed status, if any.
2020-11-06 17:51:31 +01:00
} )
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
}
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollPrayTest ( testData , game . wfrp4e . utility . getSpeaker ( cardOptions . speaker ) ) ;
2020-11-06 17:51:31 +01:00
result . postFunction = "prayerTest" ;
try {
2020-11-07 00:42:04 +01:00
let contextAudio = await game . wfrp4e . audio . MatchContextAudio ( game . wfrp4e . audio . FindContext ( result ) )
2020-11-06 17:51:31 +01:00
cardOptions . sound = contextAudio . file || cardOptions . sound
}
catch
{ }
Hooks . call ( "wfrp4e:rollPrayerTest" , result , cardOptions )
if ( ! options . suppressMessage )
2020-11-07 00:42:04 +01:00
game . wfrp4e . dice . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
game . wfrp4e . opposed . handleOpposedTarget ( msg ) // Send to handleOpposed to determine opposed status, if any.
2020-11-06 17:51:31 +01:00
} )
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
}
2020-11-07 00:42:04 +01:00
testData = await game . wfrp4e . dice . rollDices ( testData , cardOptions ) ;
let result = game . wfrp4e . dice . rollTest ( testData ) ;
2020-11-06 17:51:31 +01:00
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 )
2024-05-31 12:13:20 +02:00
foundry . utils . mergeObject ( result , testData . extra ) ;
2020-11-06 17:51:31 +01:00
try {
2020-11-07 00:42:04 +01:00
let contextAudio = await game . wfrp4e . audio . MatchContextAudio ( game . wfrp4e . audio . FindContext ( result ) )
2020-11-06 17:51:31 +01:00
cardOptions . sound = contextAudio . file || cardOptions . sound
}
catch
{ }
Hooks . call ( "wfrp4e:rollTraitTest" , result , cardOptions )
if ( ! options . suppressMessage )
2020-11-07 00:42:04 +01:00
game . wfrp4e . dice . renderRollCard ( cardOptions , result , options . rerenderMessage ) . then ( msg => {
game . wfrp4e . opposed . handleOpposedTarget ( msg ) // Send to handleOpposed to determine opposed status, if any.
2020-11-06 17:51:31 +01:00
} )
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 ( ) {
2024-05-31 12:13:20 +02:00
let preparedData = foundry . utils . duplicate ( this . data )
2020-11-06 17:51:31 +01:00
// Call prepareItems first to organize and process OwnedItems
2024-05-31 12:13:20 +02:00
foundry . utils . mergeObject ( preparedData , this . prepareItems ( ) )
2020-11-06 17:51:31 +01:00
// 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 ( ) {
2024-05-31 12:13:20 +02:00
let actorData = foundry . utils . duplicate ( this . data )
2020-11-06 17:51:31 +01:00
// 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
2020-11-06 21:44:22 +01:00
let row = game . wfrp4e . tables [ actorData . data . details . hitLocationTable . value ] . rows . find ( r => r . result == loc )
2020-11-06 17:51:31 +01:00
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" )
2020-11-06 21:44:22 +01:00
item . type = game . wfrp4e . config . trappingCategories [ item . data . trappingType . value ] ;
2020-11-06 17:51:31 +01:00
else
2020-11-06 21:44:22 +01:00
item . type = game . wfrp4e . config . trappingCategories [ item . type ] ;
2020-11-06 17:51:31 +01:00
} )
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
2020-11-06 21:44:22 +01:00
enc . penalty = game . wfrp4e . config . encumbrancePenalties [ "maxEncumbered" ] ;
2020-11-06 17:51:31 +01:00
}
else if ( enc . state > 2 ) {
enc [ "veryEncumbered" ] = true
2020-11-06 21:44:22 +01:00
enc . penalty = game . wfrp4e . config . encumbrancePenalties [ "veryEncumbered" ] ;
2020-11-06 17:51:31 +01:00
}
else if ( enc . state > 1 ) {
enc [ "encumbered" ] = true
2020-11-06 21:44:22 +01:00
enc . penalty = game . wfrp4e . config . encumbrancePenalties [ "encumbered" ] ;
2020-11-06 17:51:31 +01:00
}
else
enc [ "notEncumbered" ] = true ;
// Return all processed objects
return {
inventory ,
containers ,
2020-11-07 00:42:04 +01:00
basicSkills : basicSkills . sort ( game . wfrp4e . utility . nameSorter ) ,
advancedOrGroupedSkills : advancedOrGroupedSkills . sort ( game . wfrp4e . utility . nameSorter ) ,
2020-11-06 17:51:31 +01:00
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"
}
2020-11-06 21:44:22 +01:00
skill . data . characteristic . abrev = game . wfrp4e . config . characteristicsAbbrev [ skill . data . characteristic . value ] ;
2020-11-07 00:42:04 +01:00
skill . data . cost = game . wfrp4e . utility . _calculateAdvCost ( skill . data . advances . value , "skill" , skill . data . advances . costModifier )
2020-11-06 17:51:31 +01:00
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" ) ;
2020-11-06 21:44:22 +01:00
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" ;
2020-11-06 17:51:31 +01:00
// 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
2020-11-07 00:42:04 +01:00
weapon [ "properties" ] = game . wfrp4e . utility . _prepareQualitiesFlaws ( weapon , ! ! weapon . skillToUse ) ;
2020-11-06 17:51:31 +01:00
// 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
2020-11-07 00:42:04 +01:00
weapon . properties = game . wfrp4e . utility . _separateQualitiesFlaws ( weapon . properties ) ;
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
armor . properties = game . wfrp4e . utility . _separateQualitiesFlaws ( game . wfrp4e . utility . _prepareQualitiesFlaws ( armor ) ) ;
2020-11-06 17:51:31 +01:00
// 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 ;
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . addLayer ( AP , armor , "head" )
2020-11-06 17:51:31 +01:00
}
if ( armor . data . maxAP . body > 0 ) {
armor [ "protectsBody" ] = true ;
AP . body . value += armor . data . currentAP . body ;
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . addLayer ( AP , armor , "body" )
2020-11-06 17:51:31 +01:00
}
if ( armor . data . maxAP . lArm > 0 ) {
armor [ "protectslArm" ] = true ;
AP . lArm . value += armor . data . currentAP . lArm ;
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . addLayer ( AP , armor , "lArm" )
2020-11-06 17:51:31 +01:00
}
if ( armor . data . maxAP . rArm > 0 ) {
armor [ "protectsrArm" ] = true ;
AP . rArm . value += armor . data . currentAP . rArm ;
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . addLayer ( AP , armor , "rArm" )
2020-11-06 17:51:31 +01:00
}
if ( armor . data . maxAP . lLeg > 0 ) {
armor [ "protectslLeg" ] = true ;
AP . lLeg . value += armor . data . currentAP . lLeg ;
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . addLayer ( AP , armor , "lLeg" )
2020-11-06 17:51:31 +01:00
}
if ( armor . data . maxAP . rLeg > 0 ) {
armor [ "protectsrLeg" ] = true
AP . rLeg . value += armor . data . currentAP . rLeg ;
2020-11-07 00:42:04 +01:00
game . wfrp4e . utility . addLayer ( AP , armor , "rLeg" )
2020-11-06 17:51:31 +01:00
}
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 ;
2020-11-07 00:42:04 +01:00
let ammoProperties = game . wfrp4e . utility . _prepareQualitiesFlaws ( ammo ) ;
2020-11-06 17:51:31 +01:00
// 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" ) {
2020-11-07 00:42:04 +01:00
item . data . description . value = game . wfrp4e . utility . _spellDescription ( item ) ;
2020-11-06 17:51:31 +01:00
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' ) ) {
2020-11-06 21:44:22 +01:00
formula = formula . replace ( "bonus de " + game . wfrp4e . config . characteristics [ ch ] . toLowerCase ( ) , actorData . data . characteristics [ ch ] . bonus ) ;
2020-11-06 17:51:31 +01:00
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 ( ) {
2024-05-31 12:13:20 +02:00
let allItems = foundry . utils . duplicate ( this . data . items )
2020-11-06 17:51:31 +01:00
let ownedBasicSkills = allItems . filter ( i => i . type == "skill" && i . data . advanced . value == "bsc" ) ;
2020-11-07 00:42:04 +01:00
let allBasicSkills = await game . wfrp4e . utility . allBasicSkills ( )
2020-11-06 17:51:31 +01:00
// 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
* /
2020-11-06 21:44:22 +01:00
static applyDamage ( victim , opposeData , damageType = game . wfrp4e . config . DAMAGE _TYPE . NORMAL ) {
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
let actor = game . wfrp4e . utility . getSpeaker ( victim ) ;
let attacker = game . wfrp4e . utility . getSpeaker ( opposeData . speakerAttack )
2020-11-06 17:51:31 +01:00
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 ;
2020-11-06 21:44:22 +01:00
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 )
2020-11-06 17:51:31 +01:00
let AP = { } ;
// Start message update string
let updateMsg = ` <b> ${ game . i18n . localize ( "CHAT.DamageApplied" ) } </b><span class = 'hide-option'>: @TOTAL ` ;
2020-11-06 21:44:22 +01:00
if ( damageType != game . wfrp4e . config . DAMAGE _TYPE . IGNORE _ALL )
2020-11-06 17:51:31 +01:00
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
2020-11-07 00:42:04 +01:00
game . wfrp4e . audio . PlayContextAudio ( soundContext )
2020-11-06 17:51:31 +01:00
// 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 )
2020-11-06 21:44:22 +01:00
updateMsg += ` <br> ${ game . i18n . localize ( "CHAT.DamageAP" ) } ${ game . wfrp4e . config . locations [ opposeData . hitloc . value ] } `
2020-11-06 17:51:31 +01:00
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 {
2020-11-06 21:44:22 +01:00
skillList = game . wfrp4e . config . speciesSkills [ this . data . data . details . species . value ] ;
2020-11-06 17:51:31 +01:00
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)
2020-11-07 00:42:04 +01:00
skillList = game . wfrp4e . config . speciesSkills [ game . wfrp4e . utility . findKey ( this . data . data . details . species . value , game . wfrp4e . config . species ) ]
2020-11-06 17:51:31 +01:00
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 {
2020-11-06 21:44:22 +01:00
talentList = game . wfrp4e . config . speciesTalents [ this . data . data . details . species . value ] ;
2020-11-06 17:51:31 +01:00
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)
2020-11-07 00:42:04 +01:00
talentList = game . wfrp4e . config . speciesTalents [ game . wfrp4e . utility . findKey ( this . data . data . details . species . value , game . wfrp4e . config . species ) ]
2020-11-06 17:51:31 +01:00
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 ++ ) {
2020-11-06 21:44:22 +01:00
let result = game . wfrp4e . tables . rollTable ( "talents" )
2020-11-06 17:51:31 +01:00
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 ) {
2024-05-31 12:13:20 +02:00
existingSkill = foundry . utils . duplicate ( existingSkill ) ;
2020-11-06 17:51:31 +01:00
// 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))
2020-11-07 00:42:04 +01:00
let skillToAdd = await game . wfrp4e . utility . findSkill ( skillName )
2020-11-06 17:51:31 +01:00
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))
2020-11-07 00:42:04 +01:00
let talent = await game . wfrp4e . utility . findTalent ( talentName ) ;
2020-11-06 17:51:31 +01:00
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 } ` ;
2020-11-07 00:42:04 +01:00
ChatMessage . create ( game . wfrp4e . utility . chatDataSetup ( html ) ) ;
2020-11-06 17:51:31 +01:00
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 ;
2020-11-06 21:44:22 +01:00
new ActorWfrp4e _fr ( data . postData . actor ) [ ` ${ data . postData . postFunction } ` ] ( { testData : data . preData , cardOptions } ) ;
2020-11-06 17:51:31 +01:00
//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 ;
2020-11-06 21:44:22 +01:00
new ActorWfrp4e _fr ( data . postData . actor ) [ ` ${ data . postData . postFunction } ` ] ( { testData : newTestData , cardOptions } , { rerenderMessage : message } ) ;
2020-11-06 17:51:31 +01:00
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 } ` ;
2020-11-07 00:42:04 +01:00
ChatMessage . create ( game . wfrp4e . utility . chatDataSetup ( html ) ) ;
2020-11-06 17:51:31 +01:00
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 ;
2020-11-06 21:44:22 +01:00
new ActorWfrp4e _fr ( data . postData . actor ) [ ` ${ data . postData . postFunction } ` ] ( { testData : data . preData , cardOptions } ) ;
2020-11-06 17:51:31 +01:00
}
/ * *
* 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
2020-11-07 00:42:04 +01:00
ChatMessage . create ( game . wfrp4e . utility . chatDataSetup ( ` <b> ${ this . name } </b> gains ${ corruption } Corruption. ` , "gmroll" , false ) )
2020-11-06 17:51:31 +01:00
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 ;
2020-11-06 21:44:22 +01:00
let tableText = "Roll on a Corruption Table:<br>" + game . wfrp4e . config . corruptionTables . map ( t => ` @Table[ ${ t } ]<br> ` ) . join ( "" )
2020-11-07 00:42:04 +01:00
ChatMessage . create ( game . wfrp4e . utility . chatDataSetup ( `
2020-11-06 17:51:31 +01:00
< h3 > Dissolution of Body and Mind < / h 3 >
< 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 > l o s e s $ { w p b } C o r r u p t i o n .
< p > $ { tableText } < / p > ` ,
"gmroll" , false ) )
this . update ( { "data.status.corruption.value" : Number ( this . data . data . status . corruption . value ) - wpb } )
}
else
2020-11-07 00:42:04 +01:00
ChatMessage . create ( game . wfrp4e . utility . chatDataSetup ( ` You have managed to hold off your corruption. For now. ` , "gmroll" , false ) )
2020-11-06 17:51:31 +01:00
}
async handleExtendedTest ( testResult ) {
2024-05-31 12:13:20 +02:00
let test = foundry . utils . duplicate ( this . getEmbeddedEntity ( "OwnedItem" , testResult . options . extended ) ) ;
2020-11-06 17:51:31 +01:00
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 ) ;
}
}