2021-01-18 16:13:32 -06:00
import { simplifyRollFormula , d20Roll , damageRoll } from "../dice.js" ;
2020-10-23 13:32:23 -04:00
import AbilityUseDialog from "../apps/ability-use-dialog.js" ;
2020-06-24 14:23:26 -04:00
/ * *
* Override and extend the basic : class : ` Item ` implementation
* /
2020-10-23 13:32:23 -04:00
export default class Item5e extends Item {
2020-06-24 14:23:26 -04:00
/* -------------------------------------------- */
/* Item Properties */
/* -------------------------------------------- */
/ * *
* Determine which ability score modifier is used by this item
* @ type { string | null }
* /
get abilityMod ( ) {
const itemData = this . data . data ;
if ( ! ( "ability" in itemData ) ) return null ;
// Case 1 - defined directly by the item
2020-10-23 13:32:23 -04:00
if ( itemData . ability ) return itemData . ability ;
2020-06-24 14:23:26 -04:00
// Case 2 - inferred from a parent actor
2020-10-23 13:32:23 -04:00
else if ( this . actor ) {
2020-06-24 14:23:26 -04:00
const actorData = this . actor . data . data ;
2020-10-23 13:32:23 -04:00
2021-02-09 23:47:32 -05:00
// Powers - Use Actor powercasting modifier based on power school
if ( this . data . type === "power" ) {
switch ( this . data . data . school ) {
case "lgt" : return "wis" ;
case "uni" : return ( actorData . abilities [ "wis" ] . mod >= actorData . abilities [ "cha" ] . mod ) ? "wis" : "cha" ;
case "drk" : return "cha" ;
case "tec" : return "int" ;
}
return "none" ;
}
2020-10-23 13:32:23 -04:00
// Tools - default to Intelligence
else if ( this . data . type === "tool" ) return "int" ;
// Weapons
else if ( this . data . type === "weapon" ) {
const wt = itemData . weaponType ;
// Melee weapons - Str or Dex if Finesse (PHB pg. 147)
if ( [ "simpleVW" , "martialVW" , "simpleLW" , "martialLW" ] . includes ( wt ) ) {
if ( itemData . properties . fin === true ) { // Finesse weapons
return ( actorData . abilities [ "dex" ] . mod >= actorData . abilities [ "str" ] . mod ) ? "dex" : "str" ;
}
return "str" ;
}
// Ranged weapons - Dex (PH p.194)
else if ( [ "simpleB" , "martialB" ] . includes ( wt ) ) return "dex" ;
}
return "str" ;
2020-06-24 14:23:26 -04:00
}
// Case 3 - unknown
return null
}
/* -------------------------------------------- */
/ * *
* Does the Item implement an attack roll as part of its usage
* @ type { boolean }
* /
get hasAttack ( ) {
2020-10-23 13:32:23 -04:00
return [ "mwak" , "rwak" , "mpak" , "rpak" ] . includes ( this . data . data . actionType ) ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
* Does the Item implement a damage roll as part of its usage
* @ type { boolean }
* /
get hasDamage ( ) {
return ! ! ( this . data . data . damage && this . data . data . damage . parts . length ) ;
}
/* -------------------------------------------- */
/ * *
* Does the Item implement a versatile damage roll as part of its usage
* @ type { boolean }
* /
get isVersatile ( ) {
return ! ! ( this . hasDamage && this . data . data . damage . versatile ) ;
}
/* -------------------------------------------- */
/ * *
* Does the item provide an amount of healing instead of conventional damage ?
* @ return { boolean }
* /
get isHealing ( ) {
return ( this . data . data . actionType === "heal" ) && this . data . data . damage . parts . length ;
}
/* -------------------------------------------- */
/ * *
* Does the Item implement a saving throw as part of its usage
* @ type { boolean }
* /
get hasSave ( ) {
2021-01-18 16:13:32 -06:00
const save = this . data . data ? . save || { } ;
return ! ! ( save . ability && save . scaling ) ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
* Does the Item have a target
* @ type { boolean }
* /
get hasTarget ( ) {
const target = this . data . data . target ;
return target && ! [ "none" , "" ] . includes ( target . type ) ;
}
/* -------------------------------------------- */
/ * *
* Does the Item have an area of effect target
* @ type { boolean }
* /
get hasAreaTarget ( ) {
const target = this . data . data . target ;
return target && ( target . type in CONFIG . SW5E . areaTargetTypes ) ;
}
/* -------------------------------------------- */
/ * *
* A flag for whether this Item is limited in it ' s ability to be used by charges or by recharge .
* @ type { boolean }
* /
get hasLimitedUses ( ) {
let chg = this . data . data . recharge || { } ;
let uses = this . data . data . uses || { } ;
return ! ! chg . value || ( ! ! uses . per && ( uses . max > 0 ) ) ;
}
/* -------------------------------------------- */
/* Data Preparation */
/* -------------------------------------------- */
/ * *
* Augment the basic Item data model with additional dynamic data .
* /
prepareData ( ) {
super . prepareData ( ) ;
// Get the Item's data
const itemData = this . data ;
const data = itemData . data ;
const C = CONFIG . SW5E ;
2021-01-04 22:16:56 -06:00
const labels = this . labels = { } ;
2020-06-24 14:23:26 -04:00
// Classes
if ( itemData . type === "class" ) {
data . levels = Math . clamped ( data . levels , 1 , 20 ) ;
}
// Power Level, School, and Components
if ( itemData . type === "power" ) {
2020-11-12 17:30:07 -05:00
data . preparation . mode = data . preparation . mode || "prepared" ;
2020-06-24 14:23:26 -04:00
labels . level = C . powerLevels [ data . level ] ;
labels . school = C . powerSchools [ data . school ] ;
labels . components = Object . entries ( data . components ) . reduce ( ( arr , c ) => {
if ( c [ 1 ] !== true ) return arr ;
arr . push ( c [ 0 ] . titleCase ( ) . slice ( 0 , 1 ) ) ;
return arr ;
} , [ ] ) ;
2020-10-23 13:32:23 -04:00
labels . materials = data ? . materials ? . value ? ? null ;
2020-06-24 14:23:26 -04:00
}
// Feat Items
else if ( itemData . type === "feat" ) {
const act = data . activation ;
2020-10-23 13:32:23 -04:00
if ( act && ( act . type === C . abilityActivationTypes . legendary ) ) labels . featType = game . i18n . localize ( "SW5E.LegendaryActionLabel" ) ;
else if ( act && ( act . type === C . abilityActivationTypes . lair ) ) labels . featType = game . i18n . localize ( "SW5E.LairActionLabel" ) ;
else if ( act && act . type ) labels . featType = game . i18n . localize ( data . damage . length ? "SW5E.Attack" : "SW5E.Action" ) ;
else labels . featType = game . i18n . localize ( "SW5E.Passive" ) ;
2020-06-24 14:23:26 -04:00
}
2020-11-12 17:30:07 -05:00
// Species Items
else if ( itemData . type === "species" ) {
// labels.species = C.species[data.species];
}
// Archetype Items
else if ( itemData . type === "archetype" ) {
2020-10-23 12:36:42 -04:00
// labels.archetype = C.archetype[data.archetype];
}
2020-11-12 17:30:07 -05:00
// Background Items
else if ( itemData . type === "background" ) {
// labels.background = C.background[data.background];
}
// Class Feature Items
2020-10-23 12:36:42 -04:00
else if ( itemData . type === "classfeature" ) {
2020-11-12 17:30:07 -05:00
// labels.classFeature = C.classFeature[data.classFeature];
2021-04-06 16:03:48 -04:00
}
// Deployment Items
else if ( itemData . type === "deployment" ) {
// labels.deployment = C.deployment[data.deployment];
}
// Venture Items
else if ( itemData . type === "venture" ) {
// labels.venture = C.venture[data.venture];
}
// Fighting Style Items
else if ( itemData . type === "fightingstyle" ) {
// labels.fightingstyle = C.fightingstyle[data.fightingstyle];
}
// Fighting Mastery Items
else if ( itemData . type === "fightingmastery" ) {
// labels.fightingmastery = C.fightingmastery[data.fightingmastery];
}
// Lightsaber Form Items
else if ( itemData . type === "lightsaberform" ) {
// labels.lightsaberform = C.lightsaberform[data.lightsaberform];
}
2020-07-30 16:04:59 -04:00
2020-06-24 14:23:26 -04:00
// Equipment Items
else if ( itemData . type === "equipment" ) {
2020-10-23 13:32:23 -04:00
labels . armor = data . armor . value ? ` ${ data . armor . value } ${ game . i18n . localize ( "SW5E.AC" ) } ` : "" ;
2020-06-24 14:23:26 -04:00
}
// Activated Items
if ( data . hasOwnProperty ( "activation" ) ) {
// Ability Activation Label
let act = data . activation || { } ;
if ( act ) labels . activation = [ act . cost , C . abilityActivationTypes [ act . type ] ] . filterJoin ( " " ) ;
// Target Label
let tgt = data . target || { } ;
if ( [ "none" , "touch" , "self" ] . includes ( tgt . units ) ) tgt . value = null ;
if ( [ "none" , "self" ] . includes ( tgt . type ) ) {
tgt . value = null ;
tgt . units = null ;
}
labels . target = [ tgt . value , C . distanceUnits [ tgt . units ] , C . targetTypes [ tgt . type ] ] . filterJoin ( " " ) ;
// Range Label
let rng = data . range || { } ;
if ( [ "none" , "touch" , "self" ] . includes ( rng . units ) || ( rng . value === 0 ) ) {
rng . value = null ;
rng . long = null ;
}
labels . range = [ rng . value , rng . long ? ` / ${ rng . long } ` : null , C . distanceUnits [ rng . units ] ] . filterJoin ( " " ) ;
// Duration Label
let dur = data . duration || { } ;
if ( [ "inst" , "perm" ] . includes ( dur . units ) ) dur . value = null ;
labels . duration = [ dur . value , C . timePeriods [ dur . units ] ] . filterJoin ( " " ) ;
// Recharge Label
let chg = data . recharge || { } ;
2020-10-23 13:32:23 -04:00
labels . recharge = ` ${ game . i18n . localize ( "SW5E.Recharge" ) } [ ${ chg . value } ${ parseInt ( chg . value ) < 6 ? "+" : "" } ] ` ;
2020-06-24 14:23:26 -04:00
}
// Item Actions
if ( data . hasOwnProperty ( "actionType" ) ) {
2021-01-19 21:47:38 -05:00
// if this item is owned, we populate the label and saving throw during actor init
if ( ! this . isOwned ) {
// Saving throws
this . getSaveDC ( ) ;
2020-06-24 14:23:26 -04:00
2021-01-19 21:47:38 -05:00
// To Hit
this . getAttackToHit ( ) ;
}
2021-01-18 16:13:32 -06:00
2020-06-24 14:23:26 -04:00
// Damage
let dam = data . damage || { } ;
if ( dam . parts ) {
labels . damage = dam . parts . map ( d => d [ 0 ] ) . join ( " + " ) . replace ( /\+ -/g , "- " ) ;
labels . damageTypes = dam . parts . map ( d => C . damageTypes [ d [ 1 ] ] ) . join ( ", " ) ;
}
2021-01-18 16:13:32 -06:00
// Limited Uses
if ( this . isOwned && ! ! data . uses ? . max ) {
let max = data . uses . max ;
if ( ! Number . isNumeric ( max ) ) {
max = Roll . replaceFormulaData ( max , this . actor . getRollData ( ) ) ;
if ( Roll . MATH _PROXY . safeEval ) max = Roll . MATH _PROXY . safeEval ( max ) ;
}
data . uses . max = Number ( max ) ;
}
}
2020-06-24 14:23:26 -04:00
}
2021-01-18 16:13:32 -06:00
/* -------------------------------------------- */
2021-01-04 17:56:28 -06:00
/ * *
2021-01-18 16:13:32 -06:00
* Update the derived power DC for an item that requires a saving throw
2021-01-04 17:56:28 -06:00
* @ returns { number | null }
* /
getSaveDC ( ) {
if ( ! this . hasSave ) return ;
const save = this . data . data ? . save ;
2021-01-18 16:13:32 -06:00
// Actor power-DC based scaling
if ( save . scaling === "power" ) {
2021-02-09 23:47:32 -05:00
switch ( this . data . data . school ) {
case "lgt" : {
save . dc = this . isOwned ? getProperty ( this . actor . data , "data.attributes.powerForceLightDC" ) : null ;
break ;
}
case "uni" : {
save . dc = this . isOwned ? getProperty ( this . actor . data , "data.attributes.powerForceUnivDC" ) : null ;
break ;
}
case "drk" : {
save . dc = this . isOwned ? getProperty ( this . actor . data , "data.attributes.powerForceDarkDC" ) : null ;
break ;
}
case "tec" : {
save . dc = this . isOwned ? getProperty ( this . actor . data , "data.attributes.powerTechDC" ) : null ;
break ;
}
}
2021-01-04 17:56:28 -06:00
}
// Ability-score based scaling
else if ( save . scaling !== "flat" ) {
save . dc = this . isOwned ? getProperty ( this . actor . data , ` data.abilities. ${ save . scaling } .dc ` ) : null ;
}
// Update labels
2021-01-18 16:13:32 -06:00
const abl = CONFIG . SW5E . abilities [ save . ability ] ;
this . labels . save = game . i18n . format ( "SW5E.SaveDC" , { dc : save . dc || "" , ability : abl } ) ;
2021-01-04 17:56:28 -06:00
return save . dc ;
}
2020-06-24 14:23:26 -04:00
/* -------------------------------------------- */
2021-01-18 16:13:32 -06:00
/ * *
* Update a label to the Item detailing its total to hit bonus .
* Sources :
* - item entity ' s innate attack bonus
* - item 's actor' s proficiency bonus if applicable
* - item 's actor' s global bonuses to the given item type
* - item ' s ammunition if applicable
2021-01-19 21:47:38 -05:00
*
2021-01-18 16:13:32 -06:00
* @ returns { Object } returns ` rollData ` and ` parts ` to be used in the item ' s Attack roll
* /
getAttackToHit ( ) {
const itemData = this . data . data ;
if ( ! this . hasAttack || ! itemData ) return ;
const rollData = this . getRollData ( ) ;
// Define Roll bonuses
const parts = [ ] ;
// Include the item's innate attack bonus as the initial value and label
if ( itemData . attackBonus ) {
parts . push ( itemData . attackBonus )
this . labels . toHit = itemData . attackBonus ;
}
// Take no further action for un-owned items
if ( ! this . isOwned ) return { rollData , parts } ;
// Ability score modifier
parts . push ( ` @mod ` ) ;
// Add proficiency bonus if an explicit proficiency flag is present or for non-item features
if ( ! [ "weapon" , "consumable" ] . includes ( this . data . type ) || itemData . proficient ) {
parts . push ( "@prof" ) ;
}
// Actor-level global bonus to attack rolls
const actorBonus = this . actor . data . data . bonuses ? . [ itemData . actionType ] || { } ;
if ( actorBonus . attack ) parts . push ( actorBonus . attack ) ;
// One-time bonus provided by consumed ammunition
if ( ( itemData . consume ? . type === 'ammo' ) && ! ! this . actor . items ) {
const ammoItemData = this . actor . items . get ( itemData . consume . target ) ? . data ;
if ( ammoItemData ) {
const ammoItemQuantity = ammoItemData . data . quantity ;
const ammoCanBeConsumed = ammoItemQuantity && ( ammoItemQuantity - ( itemData . consume . amount ? ? 0 ) >= 0 ) ;
const ammoItemAttackBonus = ammoItemData . data . attackBonus ;
const ammoIsTypeConsumable = ( ammoItemData . type === "consumable" ) && ( ammoItemData . data . consumableType === "ammo" )
if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) {
parts . push ( "@ammo" ) ;
rollData [ "ammo" ] = ammoItemAttackBonus ;
}
}
}
// Condense the resulting attack bonus formula into a simplified label
let toHitLabel = simplifyRollFormula ( parts . join ( '+' ) , rollData ) . trim ( ) ;
if ( toHitLabel . charAt ( 0 ) !== '-' ) {
toHitLabel = '+ ' + toHitLabel
}
this . labels . toHit = toHitLabel ;
// Update labels and return the prepared roll data
return { rollData , parts } ;
}
/* -------------------------------------------- */
2020-06-24 14:23:26 -04:00
/ * *
* Roll the item to Chat , creating a chat card which contains follow up attack or damage roll options
2020-10-23 13:32:23 -04:00
* @ param { boolean } [ configureDialog ] Display a configuration dialog for the item roll , if applicable ?
* @ param { string } [ rollMode ] The roll display mode with which to display ( or not ) the card
* @ param { boolean } [ createMessage ] Whether to automatically create a chat message ( if true ) or simply return
* the prepared chat message data ( if false ) .
2021-01-18 16:13:32 -06:00
* @ return { Promise < ChatMessage | object | void > }
2020-06-24 14:23:26 -04:00
* /
2021-01-18 16:13:32 -06:00
async roll ( { configureDialog = true , rollMode , createMessage = true } = { } ) {
let item = this ;
const actor = this . actor ;
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
// Reference aspects of the item data necessary for usage
const id = this . data . data ; // Item data
const hasArea = this . hasAreaTarget ; // Is the ability usage an AoE?
const resource = id . consume || { } ; // Resource consumption
const recharge = id . recharge || { } ; // Recharge mechanic
const uses = id ? . uses ? ? { } ; // Limited uses
const isPower = this . type === "power" ; // Does the item require a power slot?
2021-02-09 23:47:32 -05:00
// TODO: Possibly Mod this to not consume slots based on class?
2021-01-18 16:13:32 -06:00
const requirePowerSlot = isPower && ( id . level > 0 ) && CONFIG . SW5E . powerUpcastModes . includes ( id . preparation . mode ) ;
// Define follow-up actions resulting from the item usage
let createMeasuredTemplate = hasArea ; // Trigger a template creation
let consumeRecharge = ! ! recharge . value ; // Consume recharge
2021-01-22 16:17:49 -06:00
let consumeResource = ! ! resource . target && resource . type !== "ammo" && ! [ 'simpleB' , 'martialB' ] . includes ( id . weaponType ) ; // Consume a linked (non-ammo) resource, ignore if use is from a blaster
2021-01-18 16:13:32 -06:00
let consumePowerSlot = requirePowerSlot ; // Consume a power slot
let consumeUsage = ! ! uses . per ; // Consume limited uses
let consumeQuantity = uses . autoDestroy ; // Consume quantity of the item in lieu of uses
2021-02-18 00:24:02 -05:00
// Display a configuration dialog to customize the usage
2021-01-18 16:13:32 -06:00
const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage ;
if ( configureDialog && needsConfiguration ) {
const configuration = await AbilityUseDialog . create ( this ) ;
if ( ! configuration ) return ;
2021-02-10 02:32:08 -05:00
2021-01-18 16:13:32 -06:00
// Determine consumption preferences
createMeasuredTemplate = Boolean ( configuration . placeTemplate ) ;
consumeUsage = Boolean ( configuration . consumeUse ) ;
consumeRecharge = Boolean ( configuration . consumeRecharge ) ;
consumeResource = Boolean ( configuration . consumeResource ) ;
consumePowerSlot = Boolean ( configuration . consumeSlot ) ;
// Handle power upcasting
if ( requirePowerSlot ) {
const slotLevel = configuration . level ;
2021-02-09 23:47:32 -05:00
const powerLevel = parseInt ( slotLevel ) ;
2021-02-10 15:41:02 -05:00
2021-01-18 16:13:32 -06:00
if ( powerLevel !== id . level ) {
const upcastData = mergeObject ( this . data , { "data.level" : powerLevel } , { inplace : false } ) ;
item = this . constructor . createOwned ( upcastData , actor ) ; // Replace the item with an upcast version
}
2021-02-09 23:47:32 -05:00
if ( consumePowerSlot ) consumePowerSlot = ` power ${ powerLevel } ` ;
2021-01-18 16:13:32 -06:00
}
}
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
// Determine whether the item can be used by testing for resource consumption
const usage = item . _getUsageUpdates ( { consumeRecharge , consumeResource , consumePowerSlot , consumeUsage , consumeQuantity } ) ;
2021-02-10 15:41:02 -05:00
if ( ! usage ) return ;
2021-01-18 16:13:32 -06:00
const { actorUpdates , itemUpdates , resourceUpdates } = usage ;
// Commit pending data updates
if ( ! isObjectEmpty ( itemUpdates ) ) await item . update ( itemUpdates ) ;
if ( consumeQuantity && ( item . data . data . quantity === 0 ) ) await item . delete ( ) ;
if ( ! isObjectEmpty ( actorUpdates ) ) await actor . update ( actorUpdates ) ;
if ( ! isObjectEmpty ( resourceUpdates ) ) {
const resource = actor . items . get ( id . consume ? . target ) ;
if ( resource ) await resource . update ( resourceUpdates ) ;
2020-06-24 14:23:26 -04:00
}
2021-01-18 16:13:32 -06:00
// Initiate measured template creation
if ( createMeasuredTemplate ) {
const template = game . sw5e . canvas . AbilityTemplate . fromItem ( item ) ;
if ( template ) template . drawPreview ( ) ;
}
2020-10-23 13:32:23 -04:00
2021-01-18 16:13:32 -06:00
// Create or return the Chat Message data
return item . displayCard ( { rollMode , createMessage } ) ;
}
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
/* -------------------------------------------- */
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
/ * *
* Verify that the consumed resources used by an Item are available .
* Otherwise display an error and return false .
* @ param { boolean } consumeQuantity Consume quantity of the item if other consumption modes are not available ?
* @ param { boolean } consumeRecharge Whether the item consumes the recharge mechanic
* @ param { boolean } consumeResource Whether the item consumes a limited resource
* @ param { string | boolean } consumePowerSlot A level of power slot consumed , or false
* @ param { boolean } consumeUsage Whether the item consumes a limited usage
* @ returns { object | boolean } A set of data changes to apply when the item is used , or false
* @ private
* /
_getUsageUpdates ( { consumeQuantity = false , consumeRecharge = false , consumeResource = false , consumePowerSlot = false , consumeUsage = false } ) {
// Reference item data
const id = this . data . data ;
const actorUpdates = { } ;
const itemUpdates = { } ;
const resourceUpdates = { } ;
// Consume Recharge
if ( consumeRecharge ) {
const recharge = id . recharge || { } ;
if ( recharge . charged === false ) {
ui . notifications . warn ( game . i18n . format ( "SW5E.ItemNoUses" , { name : this . name } ) ) ;
return false ;
}
itemUpdates [ "data.recharge.charged" ] = false ;
}
// Consume Limited Resource
if ( consumeResource ) {
const canConsume = this . _handleConsumeResource ( itemUpdates , actorUpdates , resourceUpdates ) ;
if ( canConsume === false ) return false ;
2020-10-23 13:32:23 -04:00
}
2021-02-11 10:01:43 -05:00
// Consume Power Slots and Force/Tech Points
2021-01-18 16:13:32 -06:00
if ( consumePowerSlot ) {
const level = this . actor ? . data . data . powers [ consumePowerSlot ] ;
2021-02-10 15:41:02 -05:00
const fp = this . actor . data . data . attributes . force . points ;
const tp = this . actor . data . data . attributes . tech . points ;
2021-02-11 10:01:43 -05:00
const powerCost = id . level + 1 ;
2021-02-18 00:24:02 -05:00
const innatePower = this . actor . data . data . attributes . powercasting === 'innate' ;
if ( ! innatePower ) {
switch ( id . school ) {
case "lgt" :
case "uni" :
case "drk" : {
const powers = Number ( level ? . fvalue ? ? 0 ) ;
if ( powers === 0 ) {
const label = game . i18n . localize ( ` SW5E.PowerLevel ${ id . level } ` ) ;
ui . notifications . warn ( game . i18n . format ( "SW5E.PowerCastNoSlots" , { name : this . name , level : label } ) ) ;
return false ;
}
actorUpdates [ ` data.powers. ${ consumePowerSlot } .fvalue ` ] = Math . max ( powers - 1 , 0 ) ;
if ( fp . temp >= powerCost ) {
actorUpdates [ "data.attributes.force.points.temp" ] = fp . temp - powerCost ;
} else {
actorUpdates [ "data.attributes.force.points.value" ] = fp . value + fp . temp - powerCost ;
actorUpdates [ "data.attributes.force.points.temp" ] = 0 ;
}
break ;
2021-02-10 15:41:02 -05:00
}
2021-02-18 00:24:02 -05:00
case "tec" : {
const powers = Number ( level ? . tvalue ? ? 0 ) ;
if ( powers === 0 ) {
const label = game . i18n . localize ( ` SW5E.PowerLevel ${ id . level } ` ) ;
ui . notifications . warn ( game . i18n . format ( "SW5E.PowerCastNoSlots" , { name : this . name , level : label } ) ) ;
return false ;
}
actorUpdates [ ` data.powers. ${ consumePowerSlot } .tvalue ` ] = Math . max ( powers - 1 , 0 ) ;
if ( tp . temp >= powerCost ) {
actorUpdates [ "data.attributes.tech.points.temp" ] = tp . temp - powerCost ;
} else {
actorUpdates [ "data.attributes.tech.points.value" ] = tp . value + tp . temp - powerCost ;
actorUpdates [ "data.attributes.tech.points.temp" ] = 0 ;
}
break ;
2021-02-10 15:41:02 -05:00
}
}
2021-01-18 16:13:32 -06:00
}
}
2021-02-10 15:41:02 -05:00
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
// Consume Limited Usage
if ( consumeUsage ) {
const uses = id . uses || { } ;
const available = Number ( uses . value ? ? 0 ) ;
let used = false ;
// Reduce usages
const remaining = Math . max ( available - 1 , 0 ) ;
if ( available >= 1 ) {
used = true ;
itemUpdates [ "data.uses.value" ] = remaining ;
}
// Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity
if ( consumeQuantity && ( ! used || ( remaining === 0 ) ) ) {
const q = Number ( id . quantity ? ? 1 ) ;
if ( q >= 1 ) {
used = true ;
itemUpdates [ "data.quantity" ] = Math . max ( q - 1 , 0 ) ;
itemUpdates [ "data.uses.value" ] = uses . max ? ? 1 ;
}
}
// If the item was not used, return a warning
if ( ! used ) {
ui . notifications . warn ( game . i18n . format ( "SW5E.ItemNoUses" , { name : this . name } ) ) ;
return false ;
}
}
// Return the configured usage
return { itemUpdates , actorUpdates , resourceUpdates } ;
2020-10-23 13:32:23 -04:00
}
/* -------------------------------------------- */
2020-11-12 17:30:07 -05:00
/ * *
2021-01-18 16:13:32 -06:00
* Handle update actions required when consuming an external resource
* @ param { object } itemUpdates An object of data updates applied to this item
* @ param { object } actorUpdates An object of data updates applied to the item owner ( Actor )
* @ param { object } resourceUpdates An object of data updates applied to a different resource item ( Item )
* @ return { boolean | void } Return false to block further progress , or return nothing to continue
2020-10-23 13:32:23 -04:00
* @ private
* /
2021-01-18 16:13:32 -06:00
_handleConsumeResource ( itemUpdates , actorUpdates , resourceUpdates ) {
const actor = this . actor ;
2020-10-23 13:32:23 -04:00
const itemData = this . data . data ;
const consume = itemData . consume || { } ;
2021-01-18 16:13:32 -06:00
if ( ! consume . type ) return ;
2020-10-23 13:32:23 -04:00
2021-01-18 16:13:32 -06:00
// No consumed target
const typeLabel = CONFIG . SW5E . abilityConsumptionTypes [ consume . type ] ;
2020-10-23 13:32:23 -04:00
if ( ! consume . target ) {
ui . notifications . warn ( game . i18n . format ( "SW5E.ConsumeWarningNoResource" , { name : this . name , type : typeLabel } ) ) ;
return false ;
}
2021-01-18 16:13:32 -06:00
// Identify the consumed resource and its current quantity
let resource = null ;
let amount = Number ( consume . amount ? ? 1 ) ;
2020-10-23 13:32:23 -04:00
let quantity = 0 ;
switch ( consume . type ) {
case "attribute" :
2021-01-18 16:13:32 -06:00
resource = getProperty ( actor . data . data , consume . target ) ;
quantity = resource || 0 ;
2020-10-23 13:32:23 -04:00
break ;
case "ammo" :
case "material" :
2021-01-18 16:13:32 -06:00
resource = actor . items . get ( consume . target ) ;
quantity = resource ? resource . data . data . quantity : 0 ;
2020-10-23 13:32:23 -04:00
break ;
case "charges" :
2021-01-18 16:13:32 -06:00
resource = actor . items . get ( consume . target ) ;
if ( ! resource ) break ;
const uses = resource . data . data . uses ;
2020-11-12 17:30:07 -05:00
if ( uses . per && uses . max ) quantity = uses . value ;
2021-01-18 16:13:32 -06:00
else if ( resource . data . data . recharge ? . value ) {
quantity = resource . data . data . recharge . charged ? 1 : 0 ;
2020-11-12 17:30:07 -05:00
amount = 1 ;
}
2020-10-23 13:32:23 -04:00
break ;
}
2021-01-18 16:13:32 -06:00
// Verify that a consumed resource is available
if ( ! resource ) {
2020-10-23 13:32:23 -04:00
ui . notifications . warn ( game . i18n . format ( "SW5E.ConsumeWarningNoSource" , { name : this . name , type : typeLabel } ) ) ;
return false ;
}
2021-01-18 16:13:32 -06:00
// Verify that the required quantity is available
2020-10-23 13:32:23 -04:00
let remaining = quantity - amount ;
2021-01-18 16:13:32 -06:00
if ( remaining < 0 ) {
2020-10-23 13:32:23 -04:00
ui . notifications . warn ( game . i18n . format ( "SW5E.ConsumeWarningNoQuantity" , { name : this . name , type : typeLabel } ) ) ;
return false ;
}
2021-01-18 16:13:32 -06:00
// Define updates to provided data objects
2020-10-23 13:32:23 -04:00
switch ( consume . type ) {
case "attribute" :
2021-01-18 16:13:32 -06:00
actorUpdates [ ` data. ${ consume . target } ` ] = remaining ;
2020-10-23 13:32:23 -04:00
break ;
case "ammo" :
case "material" :
2021-01-18 16:13:32 -06:00
resourceUpdates [ "data.quantity" ] = remaining ;
2020-10-23 13:32:23 -04:00
break ;
case "charges" :
2021-01-18 16:13:32 -06:00
const uses = resource . data . data . uses || { } ;
const recharge = resource . data . data . recharge || { } ;
if ( uses . per && uses . max ) resourceUpdates [ "data.uses.value" ] = remaining ;
else if ( recharge . value ) resourceUpdates [ "data.recharge.charged" ] = false ;
2020-11-12 17:30:07 -05:00
break ;
2020-10-23 13:32:23 -04:00
}
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
2021-01-18 16:13:32 -06:00
* Display the chat card for an Item as a Chat Message
* @ param { object } options Options which configure the display of the item chat card
* @ param { string } rollMode The message visibility mode to apply to the created card
* @ param { boolean } createMessage Whether to automatically create a ChatMessage entity ( if true ) , or only return
* the prepared message data ( if false )
2020-06-24 14:23:26 -04:00
* /
2021-01-18 16:13:32 -06:00
async displayCard ( { rollMode , createMessage = true } = { } ) {
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
// Basic template rendering data
const token = this . actor . token ;
const templateData = {
actor : this . actor ,
tokenId : token ? ` ${ token . scene . _id } . ${ token . id } ` : null ,
item : this . data ,
data : this . getChatData ( ) ,
labels : this . labels ,
hasAttack : this . hasAttack ,
isHealing : this . isHealing ,
hasDamage : this . hasDamage ,
isVersatile : this . isVersatile ,
isPower : this . data . type === "power" ,
hasSave : this . hasSave ,
hasAreaTarget : this . hasAreaTarget
} ;
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
// Render the chat card template
const templateType = [ "tool" ] . includes ( this . data . type ) ? this . data . type : "item" ;
const template = ` systems/sw5e/templates/chat/ ${ templateType } -card.html ` ;
const html = await renderTemplate ( template , templateData ) ;
// Create the ChatMessage data object
const chatData = {
user : game . user . _id ,
type : CONST . CHAT _MESSAGE _TYPES . OTHER ,
content : html ,
flavor : this . data . data . chatFlavor || this . name ,
speaker : ChatMessage . getSpeaker ( { actor : this . actor , token } ) ,
flags : { "core.canPopout" : true }
} ;
// If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
if ( ( this . data . type === "consumable" ) && ! this . actor . items . has ( this . id ) ) {
chatData . flags [ "sw5e.itemData" ] = this . data ;
2020-06-24 14:23:26 -04:00
}
2021-01-18 16:13:32 -06:00
// Apply the roll mode to adjust message visibility
ChatMessage . applyRollMode ( chatData , rollMode || game . settings . get ( "core" , "rollMode" ) ) ;
// Create the Chat Message or return its data
return createMessage ? ChatMessage . create ( chatData ) : chatData ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/* Chat Cards */
/* -------------------------------------------- */
/ * *
* Prepare an object of chat data used to display a card for the Item in the chat log
* @ param { Object } htmlOptions Options used by the TextEditor . enrichHTML function
* @ return { Object } An object of chat data to render
* /
2020-10-23 13:32:23 -04:00
getChatData ( htmlOptions = { } ) {
2020-06-24 14:23:26 -04:00
const data = duplicate ( this . data . data ) ;
const labels = this . labels ;
// Rich text description
data . description . value = TextEditor . enrichHTML ( data . description . value , htmlOptions ) ;
// Item type specific properties
const props = [ ] ;
const fn = this [ ` _ ${ this . data . type } ChatData ` ] ;
if ( fn ) fn . bind ( this ) ( data , labels , props ) ;
2021-01-18 16:13:32 -06:00
// Equipment properties
2020-06-24 14:23:26 -04:00
if ( data . hasOwnProperty ( "equipped" ) && ! [ "loot" , "tool" ] . includes ( this . data . type ) ) {
2021-01-18 16:13:32 -06:00
if ( data . attunement === CONFIG . SW5E . attunementTypes . REQUIRED ) props . push ( game . i18n . localize ( CONFIG . SW5E . attunements [ CONFIG . SW5E . attunementTypes . REQUIRED ] ) ) ;
2020-06-24 14:23:26 -04:00
props . push (
2020-10-23 13:32:23 -04:00
game . i18n . localize ( data . equipped ? "SW5E.Equipped" : "SW5E.Unequipped" ) ,
game . i18n . localize ( data . proficient ? "SW5E.Proficient" : "SW5E.NotProficient" ) ,
2020-06-24 14:23:26 -04:00
) ;
}
// Ability activation properties
if ( data . hasOwnProperty ( "activation" ) ) {
props . push (
2020-10-23 13:32:23 -04:00
labels . activation + ( data . activation ? . condition ? ` ( ${ data . activation . condition } ) ` : "" ) ,
2020-06-24 14:23:26 -04:00
labels . target ,
labels . range ,
labels . duration
) ;
}
// Filter properties and return
data . properties = props . filter ( p => ! ! p ) ;
return data ;
}
/* -------------------------------------------- */
/ * *
* Prepare chat card data for equipment type items
* @ private
* /
_equipmentChatData ( data , labels , props ) {
props . push (
CONFIG . SW5E . equipmentTypes [ data . armor . type ] ,
labels . armor || null ,
2020-10-23 13:32:23 -04:00
data . stealth . value ? game . i18n . localize ( "SW5E.StealthDisadvantage" ) : null
2020-06-24 14:23:26 -04:00
) ;
}
/* -------------------------------------------- */
/ * *
* Prepare chat card data for weapon type items
* @ private
* /
_weaponChatData ( data , labels , props ) {
props . push (
CONFIG . SW5E . weaponTypes [ data . weaponType ] ,
) ;
}
/* -------------------------------------------- */
/ * *
* Prepare chat card data for consumable type items
* @ private
* /
_consumableChatData ( data , labels , props ) {
props . push (
CONFIG . SW5E . consumableTypes [ data . consumableType ] ,
2020-10-23 13:32:23 -04:00
data . uses . value + "/" + data . uses . max + " " + game . i18n . localize ( "SW5E.Charges" )
2020-06-24 14:23:26 -04:00
) ;
data . hasCharges = data . uses . value >= 0 ;
}
/* -------------------------------------------- */
/ * *
* Prepare chat card data for tool type items
* @ private
* /
_toolChatData ( data , labels , props ) {
props . push (
CONFIG . SW5E . abilities [ data . ability ] || null ,
CONFIG . SW5E . proficiencyLevels [ data . proficient || 0 ]
) ;
}
/* -------------------------------------------- */
/ * *
2021-02-09 23:47:32 -05:00
* Prepare chat card data for loot type items
2020-06-24 14:23:26 -04:00
* @ private
* /
_lootChatData ( data , labels , props ) {
props . push (
2020-10-23 13:32:23 -04:00
game . i18n . localize ( "SW5E.ItemTypeLoot" ) ,
data . weight ? data . weight + " " + game . i18n . localize ( "SW5E.AbbreviationLbs" ) : null
2020-06-24 14:23:26 -04:00
) ;
}
/* -------------------------------------------- */
/ * *
* Render a chat card for Power type data
* @ return { Object }
* @ private
* /
_powerChatData ( data , labels , props ) {
props . push (
labels . level ,
2020-10-23 13:32:23 -04:00
labels . components + ( labels . materials ? ` ( ${ labels . materials } ) ` : "" )
2020-06-24 14:23:26 -04:00
) ;
}
/* -------------------------------------------- */
/ * *
* Prepare chat card data for items of the "Feat" type
* @ private
* /
_featChatData ( data , labels , props ) {
props . push ( data . requirements ) ;
}
/* -------------------------------------------- */
/* Item Rolls - Attack, Damage, Saves, Checks */
/* -------------------------------------------- */
/ * *
* Place an attack roll using an item ( weapon , feat , power , or equipment )
2020-10-23 13:32:23 -04:00
* Rely upon the d20Roll logic for the core implementation
2020-06-24 14:23:26 -04:00
*
2020-10-23 13:32:23 -04:00
* @ param { object } options Roll options which are configured and provided to the d20Roll function
* @ return { Promise < Roll | null > } A Promise which resolves to the created Roll instance
2020-06-24 14:23:26 -04:00
* /
2020-10-23 13:32:23 -04:00
async rollAttack ( options = { } ) {
2020-06-24 14:23:26 -04:00
const itemData = this . data . data ;
const flags = this . actor . data . flags . sw5e || { } ;
if ( ! this . hasAttack ) {
throw new Error ( "You may not place an Attack Roll with this Item." ) ;
}
2020-10-23 13:32:23 -04:00
let title = ` ${ this . name } - ${ game . i18n . localize ( "SW5E.AttackRoll" ) } ` ;
2020-06-24 14:23:26 -04:00
2021-01-18 16:13:32 -06:00
// get the parts and rollData for this item's attack
const { parts , rollData } = this . getAttackToHit ( ) ;
2020-11-12 17:30:07 -05:00
2021-01-18 16:13:32 -06:00
// Handle ammunition consumption
2020-11-12 17:30:07 -05:00
delete this . _ammo ;
2021-01-18 16:13:32 -06:00
let ammo = null ;
let ammoUpdate = null ;
2020-11-12 17:30:07 -05:00
const consume = itemData . consume ;
if ( consume ? . type === "ammo" ) {
2021-01-18 16:13:32 -06:00
ammo = this . actor . items . get ( consume . target ) ;
if ( ammo ? . data ) {
2020-11-12 17:30:07 -05:00
const q = ammo . data . data . quantity ;
const consumeAmount = consume . amount ? ? 0 ;
if ( q && ( q - consumeAmount >= 0 ) ) {
this . _ammo = ammo ;
2021-01-18 16:13:32 -06:00
title += ` [ ${ ammo . name } ] ` ;
2020-10-23 13:32:23 -04:00
}
}
2021-01-18 16:13:32 -06:00
// Get pending ammunition update
const usage = this . _getUsageUpdates ( { consumeResource : true } ) ;
if ( usage === false ) return null ;
ammoUpdate = usage . resourceUpdates || { } ;
2020-11-12 17:30:07 -05:00
}
2020-06-24 14:23:26 -04:00
// Compose roll options
2020-10-23 13:32:23 -04:00
const rollConfig = mergeObject ( {
2020-06-24 14:23:26 -04:00
parts : parts ,
actor : this . actor ,
data : rollData ,
2020-10-23 13:32:23 -04:00
title : title ,
2020-11-12 17:30:07 -05:00
flavor : title ,
2020-06-24 14:23:26 -04:00
speaker : ChatMessage . getSpeaker ( { actor : this . actor } ) ,
dialogOptions : {
width : 400 ,
top : options . event ? options . event . clientY - 80 : null ,
left : window . innerWidth - 710
2020-10-23 13:32:23 -04:00
} ,
messageData : { "flags.sw5e.roll" : { type : "attack" , itemId : this . id } }
} , options ) ;
rollConfig . event = options . event ;
2020-06-24 14:23:26 -04:00
2020-11-12 17:30:07 -05:00
// Expanded critical hit thresholds
2020-06-24 14:23:26 -04:00
if ( ( this . data . type === "weapon" ) && flags . weaponCriticalThreshold ) {
rollConfig . critical = parseInt ( flags . weaponCriticalThreshold ) ;
2020-11-12 17:30:07 -05:00
} else if ( ( this . data . type === "power" ) && flags . powerCriticalThreshold ) {
rollConfig . critical = parseInt ( flags . powerCriticalThreshold ) ;
2020-06-24 14:23:26 -04:00
}
// Elven Accuracy
if ( [ "weapon" , "power" ] . includes ( this . data . type ) ) {
if ( flags . elvenAccuracy && [ "dex" , "int" , "wis" , "cha" ] . includes ( this . abilityMod ) ) {
rollConfig . elvenAccuracy = true ;
}
}
// Apply Halfling Lucky
if ( flags . halflingLucky ) rollConfig . halflingLucky = true ;
// Invoke the d20 roll helper
2020-10-23 13:32:23 -04:00
const roll = await d20Roll ( rollConfig ) ;
if ( roll === false ) return null ;
2021-01-18 16:13:32 -06:00
// Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
if ( ammo && ! isObjectEmpty ( ammoUpdate ) ) await ammo . update ( ammoUpdate ) ;
2020-10-23 13:32:23 -04:00
return roll ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
* Place a damage roll using an item ( weapon , feat , power , or equipment )
2020-11-12 17:30:07 -05:00
* Rely upon the damageRoll logic for the core implementation .
* @ param { MouseEvent } [ event ] An event which triggered this roll , if any
2021-01-18 23:49:04 -05:00
* @ param { boolean } [ critical ] Should damage be rolled as a critical hit ?
2020-11-12 17:30:07 -05:00
* @ param { number } [ powerLevel ] If the item is a power , override the level for damage scaling
* @ param { boolean } [ versatile ] If the item is a weapon , roll damage using the versatile formula
* @ param { object } [ options ] Additional options passed to the damageRoll function
* @ return { Promise < Roll > } A Promise which resolves to the created Roll instance
2020-06-24 14:23:26 -04:00
* /
2021-01-18 16:13:32 -06:00
rollDamage ( { critical = false , event = null , powerLevel = null , versatile = false , options = { } } = { } ) {
2020-11-12 17:30:07 -05:00
if ( ! this . hasDamage ) throw new Error ( "You may not make a Damage Roll with this Item." ) ;
2020-06-24 14:23:26 -04:00
const itemData = this . data . data ;
const actorData = this . actor . data . data ;
2020-10-23 13:32:23 -04:00
const messageData = { "flags.sw5e.roll" : { type : "damage" , itemId : this . id } } ;
// Get roll data
2020-11-12 17:30:07 -05:00
const parts = itemData . damage . parts . map ( d => d [ 0 ] ) ;
2020-06-24 14:23:26 -04:00
const rollData = this . getRollData ( ) ;
if ( powerLevel ) rollData . item . level = powerLevel ;
2020-11-12 17:30:07 -05:00
// Configure the damage roll
2021-01-19 21:47:38 -05:00
const actionFlavor = game . i18n . localize ( itemData . actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll" ) ;
const title = ` ${ this . name } - ${ actionFlavor } ` ;
2020-11-12 17:30:07 -05:00
const rollConfig = {
actor : this . actor ,
2021-01-18 16:13:32 -06:00
critical : critical ? ? event ? . altKey ? ? false ,
2020-11-12 17:30:07 -05:00
data : rollData ,
2021-01-18 16:13:32 -06:00
event : event ,
fastForward : event ? event . shiftKey || event . altKey || event . ctrlKey || event . metaKey : false ,
parts : parts ,
2020-11-12 17:30:07 -05:00
title : title ,
flavor : this . labels . damageTypes . length ? ` ${ title } ( ${ this . labels . damageTypes } ) ` : title ,
speaker : ChatMessage . getSpeaker ( { actor : this . actor } ) ,
dialogOptions : {
width : 400 ,
top : event ? event . clientY - 80 : null ,
left : window . innerWidth - 710
} ,
messageData : messageData
} ;
2020-10-23 13:32:23 -04:00
// Adjust damage from versatile usage
if ( versatile && itemData . damage . versatile ) {
parts [ 0 ] = itemData . damage . versatile ;
messageData [ "flags.sw5e.roll" ] . versatile = true ;
}
// Scale damage from up-casting powers
2020-06-24 14:23:26 -04:00
if ( ( this . data . type === "power" ) ) {
2021-01-18 23:49:04 -05:00
if ( ( itemData . scaling . mode === "atwill" ) ) {
2020-10-23 13:32:23 -04:00
const level = this . actor . data . type === "character" ? actorData . details . level : actorData . details . powerLevel ;
2021-01-18 23:49:04 -05:00
this . _scaleAtWillDamage ( parts , itemData . scaling . formula , level , rollData ) ;
2020-10-23 13:32:23 -04:00
}
else if ( powerLevel && ( itemData . scaling . mode === "level" ) && itemData . scaling . formula ) {
const scaling = itemData . scaling . formula ;
this . _scalePowerDamage ( parts , itemData . level , powerLevel , scaling , rollData ) ;
2020-06-24 14:23:26 -04:00
}
}
2020-11-12 17:30:07 -05:00
// Add damage bonus formula
2020-10-23 13:32:23 -04:00
const actorBonus = getProperty ( actorData , ` bonuses. ${ itemData . actionType } ` ) || { } ;
2020-11-12 17:30:07 -05:00
if ( actorBonus . damage && ( parseInt ( actorBonus . damage ) !== 0 ) ) {
parts . push ( actorBonus . damage ) ;
2020-06-24 14:23:26 -04:00
}
2021-01-18 16:13:32 -06:00
// Handle ammunition damage
const ammoData = this . _ammo ? . data ;
// only add the ammunition damage if the ammution is a consumable with type 'ammo'
if ( this . _ammo && ( ammoData . type === "consumable" ) && ( ammoData . data . consumableType === "ammo" ) ) {
2020-10-23 13:32:23 -04:00
parts . push ( "@ammo" ) ;
2021-01-18 16:13:32 -06:00
rollData [ "ammo" ] = ammoData . data . damage . parts . map ( p => p [ 0 ] ) . join ( "+" ) ;
2020-11-12 17:30:07 -05:00
rollConfig . flavor += ` [ ${ this . _ammo . name } ] ` ;
2020-10-23 13:32:23 -04:00
delete this . _ammo ;
}
2020-11-12 17:30:07 -05:00
// Scale melee critical hit damage
if ( itemData . actionType === "mwak" ) {
rollConfig . criticalBonusDice = this . actor . getFlag ( "sw5e" , "meleeCriticalDamageDice" ) ? ? 0 ;
}
2020-06-24 14:23:26 -04:00
// Call the roll helper utility
2020-11-12 17:30:07 -05:00
return damageRoll ( mergeObject ( rollConfig , options ) ) ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
2020-10-23 13:32:23 -04:00
* Adjust an at - will damage formula to scale it for higher level characters and monsters
2020-06-24 14:23:26 -04:00
* @ private
* /
2020-10-23 13:32:23 -04:00
_scaleAtWillDamage ( parts , scale , level , rollData ) {
2020-06-24 14:23:26 -04:00
const add = Math . floor ( ( level + 1 ) / 6 ) ;
if ( add === 0 ) return ;
2020-11-12 17:30:07 -05:00
this . _scaleDamage ( parts , scale || parts . join ( " + " ) , add , rollData ) ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
* Adjust the power damage formula to scale it for power level up - casting
* @ param { Array } parts The original damage parts
* @ param { number } baseLevel The default power level
* @ param { number } powerLevel The casted power level
* @ param { string } formula The scaling formula
2020-10-23 13:32:23 -04:00
* @ param { object } rollData A data object that should be applied to the scaled damage roll
* @ return { string [ ] } The scaled roll parts
2020-06-24 14:23:26 -04:00
* @ private
* /
2020-10-23 13:32:23 -04:00
_scalePowerDamage ( parts , baseLevel , powerLevel , formula , rollData ) {
2020-06-24 14:23:26 -04:00
const upcastLevels = Math . max ( powerLevel - baseLevel , 0 ) ;
if ( upcastLevels === 0 ) return parts ;
2020-11-12 17:30:07 -05:00
this . _scaleDamage ( parts , formula , upcastLevels , rollData ) ;
2020-10-23 13:32:23 -04:00
}
/* -------------------------------------------- */
/ * *
* Scale an array of damage parts according to a provided scaling formula and scaling multiplier
* @ param { string [ ] } parts Initial roll parts
* @ param { string } scaling A scaling formula
* @ param { number } times A number of times to apply the scaling formula
* @ param { object } rollData A data object that should be applied to the scaled damage roll
* @ return { string [ ] } The scaled roll parts
* @ private
* /
_scaleDamage ( parts , scaling , times , rollData ) {
if ( times <= 0 ) return parts ;
const p0 = new Roll ( parts [ 0 ] , rollData ) ;
const s = new Roll ( scaling , rollData ) . alter ( times ) ;
// Attempt to simplify by combining like dice terms
let simplified = false ;
if ( ( s . terms [ 0 ] instanceof Die ) && ( s . terms . length === 1 ) ) {
const d0 = p0 . terms [ 0 ] ;
const s0 = s . terms [ 0 ] ;
if ( ( d0 instanceof Die ) && ( d0 . faces === s0 . faces ) && d0 . modifiers . equals ( s0 . modifiers ) ) {
d0 . number += s0 . number ;
parts [ 0 ] = p0 . formula ;
simplified = true ;
}
}
// Otherwise add to the first part
if ( ! simplified ) {
parts [ 0 ] = ` ${ parts [ 0 ] } + ${ s . formula } ` ;
}
2020-06-24 14:23:26 -04:00
return parts ;
}
/* -------------------------------------------- */
/ * *
* Place an attack roll using an item ( weapon , feat , power , or equipment )
2020-10-23 13:32:23 -04:00
* Rely upon the d20Roll logic for the core implementation
2020-11-12 17:30:07 -05:00
*
2020-10-23 13:32:23 -04:00
* @ return { Promise < Roll > } A Promise which resolves to the created Roll instance
2020-06-24 14:23:26 -04:00
* /
async rollFormula ( options = { } ) {
if ( ! this . data . data . formula ) {
throw new Error ( "This Item does not have a formula to roll!" ) ;
}
// Define Roll Data
const rollData = this . getRollData ( ) ;
2020-10-23 13:32:23 -04:00
if ( options . powerLevel ) rollData . item . level = options . powerLevel ;
const title = ` ${ this . name } - ${ game . i18n . localize ( "SW5E.OtherFormula" ) } ` ;
2020-06-24 14:23:26 -04:00
// Invoke the roll and submit it to chat
const roll = new Roll ( rollData . item . formula , rollData ) . roll ( ) ;
roll . toMessage ( {
speaker : ChatMessage . getSpeaker ( { actor : this . actor } ) ,
2020-11-12 17:30:07 -05:00
flavor : title ,
2020-10-23 13:32:23 -04:00
rollMode : game . settings . get ( "core" , "rollMode" ) ,
messageData : { "flags.sw5e.roll" : { type : "other" , itemId : this . id } }
2020-06-24 14:23:26 -04:00
} ) ;
return roll ;
}
/* -------------------------------------------- */
/ * *
* Perform an ability recharge test for an item which uses the d6 recharge mechanic
2020-10-23 13:32:23 -04:00
* @ return { Promise < Roll > } A Promise which resolves to the created Roll instance
2020-06-24 14:23:26 -04:00
* /
2020-10-23 13:32:23 -04:00
async rollRecharge ( ) {
2020-06-24 14:23:26 -04:00
const data = this . data . data ;
if ( ! data . recharge . value ) return ;
// Roll the check
const roll = new Roll ( "1d6" ) . roll ( ) ;
const success = roll . total >= parseInt ( data . recharge . value ) ;
// Display a Chat Message
const promises = [ roll . toMessage ( {
2020-10-23 13:32:23 -04:00
flavor : ` ${ game . i18n . format ( "SW5E.ItemRechargeCheck" , { name : this . name } )} - ${ game . i18n . localize ( success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure" ) } ` ,
2020-06-24 14:23:26 -04:00
speaker : ChatMessage . getSpeaker ( { actor : this . actor , token : this . actor . token } )
} ) ] ;
// Update the Item data
if ( success ) promises . push ( this . update ( { "data.recharge.charged" : true } ) ) ;
return Promise . all ( promises ) . then ( ( ) => roll ) ;
}
/* -------------------------------------------- */
/ * *
2020-10-23 13:32:23 -04:00
* Roll a Tool Check . Rely upon the d20Roll logic for the core implementation
* @ prarm { Object } options Roll configuration options provided to the d20Roll function
* @ return { Promise < Roll > } A Promise which resolves to the created Roll instance
2020-06-24 14:23:26 -04:00
* /
rollToolCheck ( options = { } ) {
if ( this . type !== "tool" ) throw "Wrong item type!" ;
// Prepare roll data
let rollData = this . getRollData ( ) ;
const parts = [ ` @mod ` , "@prof" ] ;
2020-10-23 13:32:23 -04:00
const title = ` ${ this . name } - ${ game . i18n . localize ( "SW5E.ToolCheck" ) } ` ;
2020-06-24 14:23:26 -04:00
2020-10-23 13:32:23 -04:00
// Compose the roll data
const rollConfig = mergeObject ( {
2020-06-24 14:23:26 -04:00
parts : parts ,
data : rollData ,
template : "systems/sw5e/templates/chat/tool-roll-dialog.html" ,
title : title ,
speaker : ChatMessage . getSpeaker ( { actor : this . actor } ) ,
2020-11-12 17:30:07 -05:00
flavor : title ,
2020-06-24 14:23:26 -04:00
dialogOptions : {
width : 400 ,
top : options . event ? options . event . clientY - 80 : null ,
left : window . innerWidth - 710 ,
} ,
2020-10-23 13:32:23 -04:00
halflingLucky : this . actor . getFlag ( "sw5e" , "halflingLucky" ) || false ,
2021-01-18 16:13:32 -06:00
reliableTalent : ( this . data . data . proficient >= 1 ) && this . actor . getFlag ( "sw5e" , "reliableTalent" ) ,
2020-10-23 13:32:23 -04:00
messageData : { "flags.sw5e.roll" : { type : "tool" , itemId : this . id } }
} , options ) ;
rollConfig . event = options . event ;
// Call the roll helper utility
return d20Roll ( rollConfig ) ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
* Prepare a data object which is passed to any Roll formulas which are created related to this Item
* @ private
* /
getRollData ( ) {
if ( ! this . actor ) return null ;
const rollData = this . actor . getRollData ( ) ;
rollData . item = duplicate ( this . data . data ) ;
// Include an ability score modifier if one exists
const abl = this . abilityMod ;
if ( abl ) {
const ability = rollData . abilities [ abl ] ;
rollData [ "mod" ] = ability . mod || 0 ;
}
// Include a proficiency score
2020-11-12 17:30:07 -05:00
const prof = ( "proficient" in rollData . item ) ? ( rollData . item . proficient || 0 ) : 1 ;
rollData [ "prof" ] = Math . floor ( prof * ( rollData . attributes . prof || 0 ) ) ;
2020-06-24 14:23:26 -04:00
return rollData ;
}
/* -------------------------------------------- */
/* Chat Message Helpers */
/* -------------------------------------------- */
static chatListeners ( html ) {
html . on ( 'click' , '.card-buttons button' , this . _onChatCardAction . bind ( this ) ) ;
html . on ( 'click' , '.item-name' , this . _onChatCardToggleContent . bind ( this ) ) ;
}
/* -------------------------------------------- */
/ * *
* Handle execution of a chat card action via a click event on one of the card buttons
* @ param { Event } event The originating click event
* @ returns { Promise } A promise which resolves once the handler workflow is complete
* @ private
* /
static async _onChatCardAction ( event ) {
event . preventDefault ( ) ;
// Extract card data
const button = event . currentTarget ;
button . disabled = true ;
const card = button . closest ( ".chat-card" ) ;
const messageId = card . closest ( ".message" ) . dataset . messageId ;
const message = game . messages . get ( messageId ) ;
const action = button . dataset . action ;
// Validate permission to proceed with the roll
const isTargetted = action === "save" ;
if ( ! ( isTargetted || game . user . isGM || message . isAuthor ) ) return ;
2020-10-23 13:32:23 -04:00
// Recover the actor for the chat card
2020-06-24 14:23:26 -04:00
const actor = this . _getChatCardActor ( card ) ;
if ( ! actor ) return ;
2020-10-23 13:32:23 -04:00
// Get the Item from stored flag data or by the item ID on the Actor
const storedData = message . getFlag ( "sw5e" , "itemData" ) ;
const item = storedData ? this . createOwned ( storedData , actor ) : actor . getOwnedItem ( card . dataset . itemId ) ;
2020-06-24 14:23:26 -04:00
if ( ! item ) {
2020-10-23 13:32:23 -04:00
return ui . notifications . error ( game . i18n . format ( "SW5E.ActionWarningNoItem" , { item : card . dataset . itemId , name : actor . name } ) )
2020-06-24 14:23:26 -04:00
}
const powerLevel = parseInt ( card . dataset . powerLevel ) || null ;
2020-10-23 13:32:23 -04:00
// Handle different actions
switch ( action ) {
case "attack" :
await item . rollAttack ( { event } ) ; break ;
case "damage" :
case "versatile" :
2021-01-18 16:13:32 -06:00
await item . rollDamage ( {
critical : event . altKey ,
event : event ,
powerLevel : powerLevel ,
versatile : action === "versatile"
} ) ;
break ;
2020-10-23 13:32:23 -04:00
case "formula" :
await item . rollFormula ( { event , powerLevel } ) ; break ;
case "save" :
const targets = this . _getChatCardTargets ( card ) ;
for ( let token of targets ) {
const speaker = ChatMessage . getSpeaker ( { scene : canvas . scene , token : token } ) ;
await token . actor . rollAbilitySave ( button . dataset . ability , { event , speaker } ) ;
}
break ;
case "toolCheck" :
await item . rollToolCheck ( { event } ) ; break ;
case "placeTemplate" :
2021-01-18 16:13:32 -06:00
const template = game . sw5e . canvas . AbilityTemplate . fromItem ( item ) ;
2020-10-23 13:32:23 -04:00
if ( template ) template . drawPreview ( ) ;
break ;
2020-06-24 14:23:26 -04:00
}
// Re-enable the button
button . disabled = false ;
}
/* -------------------------------------------- */
/ * *
* Handle toggling the visibility of chat card content when the name is clicked
* @ param { Event } event The originating click event
* @ private
* /
static _onChatCardToggleContent ( event ) {
event . preventDefault ( ) ;
const header = event . currentTarget ;
const card = header . closest ( ".chat-card" ) ;
const content = card . querySelector ( ".card-content" ) ;
content . style . display = content . style . display === "none" ? "block" : "none" ;
}
/* -------------------------------------------- */
/ * *
* Get the Actor which is the author of a chat card
* @ param { HTMLElement } card The chat card being used
* @ return { Actor | null } The Actor entity or null
* @ private
* /
static _getChatCardActor ( card ) {
// Case 1 - a synthetic actor from a Token
const tokenKey = card . dataset . tokenId ;
if ( tokenKey ) {
const [ sceneId , tokenId ] = tokenKey . split ( "." ) ;
const scene = game . scenes . get ( sceneId ) ;
if ( ! scene ) return null ;
const tokenData = scene . getEmbeddedEntity ( "Token" , tokenId ) ;
if ( ! tokenData ) return null ;
const token = new Token ( tokenData ) ;
return token . actor ;
}
// Case 2 - use Actor ID directory
const actorId = card . dataset . actorId ;
return game . actors . get ( actorId ) || null ;
}
/* -------------------------------------------- */
/ * *
* Get the Actor which is the author of a chat card
* @ param { HTMLElement } card The chat card being used
* @ return { Array . < Actor > } An Array of Actor entities , if any
* @ private
* /
static _getChatCardTargets ( card ) {
2020-10-23 13:32:23 -04:00
let targets = canvas . tokens . controlled . filter ( t => ! ! t . actor ) ;
if ( ! targets . length && game . user . character ) targets = targets . concat ( game . user . character . getActiveTokens ( ) ) ;
if ( ! targets . length ) ui . notifications . warn ( game . i18n . localize ( "SW5E.ActionWarningNoToken" ) ) ;
2020-06-24 14:23:26 -04:00
return targets ;
}
2020-11-12 17:30:07 -05:00
2020-10-23 13:32:23 -04:00
/* -------------------------------------------- */
/* Factory Methods */
/* -------------------------------------------- */
/ * *
* Create a consumable power scroll Item from a power Item .
* @ param { Item5e } power The power to be made into a scroll
* @ return { Item5e } The created scroll consumable item
* @ private
* /
static async createScrollFromPower ( power ) {
// Get power data
const itemData = power instanceof Item5e ? power . data : power ;
const { actionType , description , source , activation , duration , target , range , damage , save , level } = itemData . data ;
// Get scroll data
const scrollUuid = CONFIG . SW5E . powerScrollIds [ level ] ;
const scrollItem = await fromUuid ( scrollUuid ) ;
const scrollData = scrollItem . data ;
delete scrollData . _id ;
// Split the scroll description into an intro paragraph and the remaining details
const scrollDescription = scrollData . data . description . value ;
const pdel = '</p>' ;
const scrollIntroEnd = scrollDescription . indexOf ( pdel ) ;
const scrollIntro = scrollDescription . slice ( 0 , scrollIntroEnd + pdel . length ) ;
const scrollDetails = scrollDescription . slice ( scrollIntroEnd + pdel . length ) ;
// Create a composite description from the scroll description and the power details
const desc = ` ${ scrollIntro } <hr/><h3> ${ itemData . name } (Level ${ level } )</h3><hr/> ${ description . value } <hr/><h3>Scroll Details</h3><hr/> ${ scrollDetails } ` ;
// Create the power scroll data
const powerScrollData = mergeObject ( scrollData , {
name : ` ${ game . i18n . localize ( "SW5E.PowerScroll" ) } : ${ itemData . name } ` ,
img : itemData . img ,
data : {
"description.value" : desc . trim ( ) ,
source ,
actionType ,
activation ,
duration ,
target ,
range ,
damage ,
save ,
level
}
} ) ;
return new this ( powerScrollData ) ;
}
2020-06-24 14:23:26 -04:00
}