2020-10-23 13:32:23 -04:00
import TraitSelector from "../apps/trait-selector.js" ;
2020-11-12 17:30:07 -05:00
import { onManageActiveEffect , prepareActiveEffectCategories } from "../effects.js" ;
2020-06-24 14:23:26 -04:00
/ * *
2020-10-23 13:32:23 -04:00
* Override and extend the core ItemSheet implementation to handle specific item types
* @ extends { ItemSheet }
2020-06-24 14:23:26 -04:00
* /
2020-10-23 13:32:23 -04:00
export default class ItemSheet5e extends ItemSheet {
constructor ( ... args ) {
super ( ... args ) ;
2020-11-12 17:30:07 -05:00
// Expand the default size of the class sheet
2020-10-23 13:32:23 -04:00
if ( this . object . data . type === "class" ) {
2020-11-12 17:30:07 -05:00
this . options . width = this . position . width = 600 ;
this . options . height = this . position . height = 680 ;
2020-10-23 13:32:23 -04:00
}
}
/* -------------------------------------------- */
2020-06-24 14:23:26 -04:00
/** @override */
static get defaultOptions ( ) {
return mergeObject ( super . defaultOptions , {
width : 560 ,
2020-11-12 17:30:07 -05:00
height : 400 ,
2020-06-24 14:23:26 -04:00
classes : [ "sw5e" , "sheet" , "item" ] ,
2020-10-23 13:32:23 -04:00
resizable : true ,
2020-06-24 14:23:26 -04:00
scrollY : [ ".tab.details" ] ,
tabs : [ { navSelector : ".tabs" , contentSelector : ".sheet-body" , initial : "description" } ]
} ) ;
}
/* -------------------------------------------- */
/** @override */
get template ( ) {
const path = "systems/sw5e/templates/items/" ;
return ` ${ path } / ${ this . item . data . type } .html ` ;
}
/* -------------------------------------------- */
/** @override */
2020-11-12 17:30:07 -05:00
async getData ( options ) {
const data = super . getData ( options ) ;
2020-06-24 14:23:26 -04:00
data . labels = this . item . labels ;
data . config = CONFIG . SW5E ;
// Item Type, Status, and Details
2020-11-12 17:30:07 -05:00
data . itemType = game . i18n . localize ( ` ITEM.Type ${ data . item . type . titleCase ( ) } ` ) ;
2020-06-24 14:23:26 -04:00
data . itemStatus = this . _getItemStatus ( data . item ) ;
data . itemProperties = this . _getItemProperties ( data . item ) ;
data . isPhysical = data . item . data . hasOwnProperty ( "quantity" ) ;
2020-11-12 17:30:07 -05:00
2020-10-23 13:32:23 -04:00
// Potential consumption targets
data . abilityConsumptionTargets = this . _getItemConsumptionTargets ( data . item ) ;
2020-06-24 14:23:26 -04:00
// Action Details
data . hasAttackRoll = this . item . hasAttack ;
data . isHealing = data . item . data . actionType === "heal" ;
data . isFlatDC = getProperty ( data . item . data , "save.scaling" ) === "flat" ;
2020-11-12 17:30:07 -05:00
data . isLine = [ "line" , "wall" ] . includes ( data . item . data . target ? . type ) ;
2020-10-23 13:32:23 -04:00
2021-01-18 23:49:04 -05:00
// Original maximum uses formula
if ( this . item . _data . data ? . uses ? . max ) data . data . uses . max = this . item . _data . data . uses . max ;
2020-10-23 13:32:23 -04:00
// Vehicles
data . isCrewed = data . item . data . activation ? . type === 'crew' ;
data . isMountable = this . _isItemMountable ( data . item ) ;
2020-11-12 17:30:07 -05:00
// Prepare Active Effects
data . effects = prepareActiveEffectCategories ( this . entity . effects ) ;
2020-06-24 14:23:26 -04:00
return data ;
}
2020-10-23 13:32:23 -04:00
/* -------------------------------------------- */
2020-11-12 17:30:07 -05:00
/ * *
2020-10-23 13:32:23 -04:00
* Get the valid item consumption targets which exist on the actor
* @ param { Object } item Item data for the item being displayed
* @ return { { string : string } } An object of potential consumption targets
* @ private
* /
_getItemConsumptionTargets ( item ) {
const consume = item . data . consume || { } ;
if ( ! consume . type ) return [ ] ;
const actor = this . item . actor ;
if ( ! actor ) return { } ;
// Ammunition
if ( consume . type === "ammo" ) {
return actor . itemTypes . consumable . reduce ( ( ammo , i ) => {
if ( i . data . data . consumableType === "ammo" ) {
ammo [ i . id ] = ` ${ i . name } ( ${ i . data . data . quantity } ) ` ;
}
return ammo ;
2021-01-18 23:49:04 -05:00
} , { [ item . _id ] : ` ${ item . name } ( ${ item . data . quantity } ) ` } ) ;
2020-10-23 13:32:23 -04:00
}
// Attributes
else if ( consume . type === "attribute" ) {
const attributes = Object . values ( CombatTrackerConfig . prototype . getAttributeChoices ( ) ) [ 0 ] ; // Bit of a hack
return attributes . reduce ( ( obj , a ) => {
obj [ a ] = a ;
return obj ;
} , { } ) ;
}
// Materials
else if ( consume . type === "material" ) {
return actor . items . reduce ( ( obj , i ) => {
if ( [ "consumable" , "loot" ] . includes ( i . data . type ) && ! i . data . data . activation ) {
obj [ i . id ] = ` ${ i . name } ( ${ i . data . data . quantity } ) ` ;
}
return obj ;
} , { } ) ;
}
// Charges
else if ( consume . type === "charges" ) {
return actor . items . reduce ( ( obj , i ) => {
2020-11-12 17:30:07 -05:00
// Limited-use items
2020-10-23 13:32:23 -04:00
const uses = i . data . data . uses || { } ;
if ( uses . per && uses . max ) {
const label = uses . per === "charges" ?
` ( ${ game . i18n . format ( "SW5E.AbilityUseChargesLabel" , { value : uses . value } )}) ` :
` ( ${ game . i18n . format ( "SW5E.AbilityUseConsumableLabel" , { max : uses . max , per : uses . per } )}) ` ;
obj [ i . id ] = i . name + label ;
}
2020-11-12 17:30:07 -05:00
// Recharging items
const recharge = i . data . data . recharge || { } ;
if ( recharge . value ) obj [ i . id ] = ` ${ i . name } ( ${ game . i18n . format ( "SW5E.Recharge" ) } ) ` ;
2020-10-23 13:32:23 -04:00
return obj ;
} , { } )
}
else return { } ;
}
2020-06-24 14:23:26 -04:00
/* -------------------------------------------- */
/ * *
* Get the text item status which is shown beneath the Item type in the top - right corner of the sheet
* @ return { string }
* @ private
* /
_getItemStatus ( item ) {
if ( item . type === "power" ) {
return CONFIG . SW5E . powerPreparationModes [ item . data . preparation ] ;
}
else if ( [ "weapon" , "equipment" ] . includes ( item . type ) ) {
2020-10-23 13:32:23 -04:00
return game . i18n . localize ( item . data . equipped ? "SW5E.Equipped" : "SW5E.Unequipped" ) ;
2020-06-24 14:23:26 -04:00
}
else if ( item . type === "tool" ) {
2020-10-23 13:32:23 -04:00
return game . i18n . localize ( item . data . proficient ? "SW5E.Proficient" : "SW5E.NotProficient" ) ;
2020-06-24 14:23:26 -04:00
}
}
/* -------------------------------------------- */
/ * *
* Get the Array of item properties which are used in the small sidebar of the description tab
* @ return { Array }
* @ private
* /
_getItemProperties ( item ) {
const props = [ ] ;
const labels = this . item . labels ;
if ( item . type === "weapon" ) {
props . push ( ... Object . entries ( item . data . properties )
. filter ( e => e [ 1 ] === true )
. map ( e => CONFIG . SW5E . weaponProperties [ e [ 0 ] ] ) ) ;
}
else if ( item . type === "power" ) {
props . push (
labels . components ,
labels . materials ,
2020-10-23 13:32:23 -04:00
item . data . components . concentration ? game . i18n . localize ( "SW5E.Concentration" ) : null ,
item . data . components . ritual ? game . i18n . localize ( "SW5E.Ritual" ) : null
2020-06-24 14:23:26 -04:00
)
}
else if ( item . type === "equipment" ) {
props . push ( CONFIG . SW5E . equipmentTypes [ item . data . armor . type ] ) ;
props . push ( labels . armor ) ;
}
else if ( item . type === "feat" ) {
props . push ( labels . featType ) ;
}
2020-07-30 16:04:59 -04:00
else if ( item . type === "species" ) {
2020-11-12 17:30:07 -05:00
//props.push(labels.species);
2020-07-30 16:04:59 -04:00
}
2020-10-23 12:36:42 -04:00
else if ( item . type === "archetype" ) {
2020-11-12 17:30:07 -05:00
//props.push(labels.archetype);
}
else if ( item . type === "background" ) {
//props.push(labels.background);
}
else if ( item . type === "classfeature" ) {
//props.push(labels.classfeature);
}
else if ( item . type === "fightingmastery" ) {
//props.push(labels.fightingmastery);
}
else if ( item . type === "fightingstyle" ) {
//props.push(labels.fightingstyle);
}
else if ( item . type === "lightsaberform" ) {
//props.push(labels.lightsaberform);
2020-10-23 12:36:42 -04:00
}
2020-10-29 20:30:50 -04:00
2020-06-24 14:23:26 -04:00
// Action type
if ( item . data . actionType ) {
props . push ( CONFIG . SW5E . itemActionTypes [ item . data . actionType ] ) ;
}
// Action usage
if ( ( item . type !== "weapon" ) && item . data . activation && ! isObjectEmpty ( item . data . activation ) ) {
props . push (
labels . activation ,
labels . range ,
labels . target ,
labels . duration
)
}
return props . filter ( p => ! ! p ) ;
}
/* -------------------------------------------- */
2020-10-23 13:32:23 -04:00
/ * *
* Is this item a separate large object like a siege engine or vehicle
* component that is usually mounted on fixtures rather than equipped , and
* has its own AC and HP .
* @ param item
* @ returns { boolean }
* @ private
* /
_isItemMountable ( item ) {
const data = item . data ;
return ( item . type === 'weapon' && data . weaponType === 'siege' )
|| ( item . type === 'equipment' && data . armor . type === 'vehicle' ) ;
}
/* -------------------------------------------- */
2020-06-24 14:23:26 -04:00
/** @override */
setPosition ( position = { } ) {
2020-11-12 17:30:07 -05:00
if ( ! ( this . _minimized || position . height ) ) {
position . height = ( this . _tabs [ 0 ] . active === "details" ) ? "auto" : this . options . height ;
2020-10-23 13:32:23 -04:00
}
2020-06-24 14:23:26 -04:00
return super . setPosition ( position ) ;
}
/* -------------------------------------------- */
/* Form Submission */
/* -------------------------------------------- */
/** @override */
2020-11-12 17:30:07 -05:00
_getSubmitData ( updateData = { } ) {
2020-06-24 14:23:26 -04:00
2020-11-12 17:30:07 -05:00
// Create the expanded update data object
const fd = new FormDataExtended ( this . form , { editors : this . editors } ) ;
let data = fd . toObject ( ) ;
if ( updateData ) data = mergeObject ( data , updateData ) ;
else data = expandObject ( data ) ;
2020-07-13 19:51:27 -03:00
2020-11-12 17:30:07 -05:00
// Handle Damage array
const damage = data . data ? . damage ;
2020-10-23 13:32:23 -04:00
if ( damage ) damage . parts = Object . values ( damage ? . parts || { } ) . map ( d => [ d [ 0 ] || "" , d [ 1 ] || "" ] ) ;
2020-07-13 19:51:27 -03:00
2020-11-12 17:30:07 -05:00
// Return the flattened submission data
return flattenObject ( data ) ;
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/** @override */
activateListeners ( html ) {
super . activateListeners ( html ) ;
2020-11-12 17:30:07 -05:00
if ( this . isEditable ) {
html . find ( ".damage-control" ) . click ( this . _onDamageControl . bind ( this ) ) ;
html . find ( '.trait-selector.class-skills' ) . click ( this . _onConfigureClassSkills . bind ( this ) ) ;
html . find ( ".effect-control" ) . click ( ev => {
if ( this . item . isOwned ) return ui . notifications . warn ( "Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update." )
onManageActiveEffect ( ev , this . item )
} ) ;
}
2020-06-24 14:23:26 -04:00
}
/* -------------------------------------------- */
/ * *
* Add or remove a damage part from the damage formula
* @ param { Event } event The original click event
* @ return { Promise }
* @ private
* /
async _onDamageControl ( event ) {
event . preventDefault ( ) ;
const a = event . currentTarget ;
// Add new damage component
if ( a . classList . contains ( "add-damage" ) ) {
await this . _onSubmit ( event ) ; // Submit any unsaved changes
const damage = this . item . data . data . damage ;
return this . item . update ( { "data.damage.parts" : damage . parts . concat ( [ [ "" , "" ] ] ) } ) ;
}
// Remove a damage component
if ( a . classList . contains ( "delete-damage" ) ) {
await this . _onSubmit ( event ) ; // Submit any unsaved changes
const li = a . closest ( ".damage-part" ) ;
const damage = duplicate ( this . item . data . data . damage ) ;
damage . parts . splice ( Number ( li . dataset . damagePart ) , 1 ) ;
return this . item . update ( { "data.damage.parts" : damage . parts } ) ;
}
}
/* -------------------------------------------- */
/ * *
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @ param { Event } event The click event which originated the selection
* @ private
* /
_onConfigureClassSkills ( event ) {
event . preventDefault ( ) ;
const skills = this . item . data . data . skills ;
const choices = skills . choices && skills . choices . length ? skills . choices : Object . keys ( CONFIG . SW5E . skills ) ;
const a = event . currentTarget ;
const label = a . parentElement ;
// Render the Trait Selector dialog
new TraitSelector ( this . item , {
2021-01-18 23:49:04 -05:00
name : a . dataset . target ,
2020-06-24 14:23:26 -04:00
title : label . innerText ,
choices : Object . entries ( CONFIG . SW5E . skills ) . reduce ( ( obj , e ) => {
if ( choices . includes ( e [ 0 ] ) ) obj [ e [ 0 ] ] = e [ 1 ] ;
return obj ;
} , { } ) ,
minimum : skills . number ,
maximum : skills . number
} ) . render ( true )
}
2020-11-12 17:30:07 -05:00
/* -------------------------------------------- */
/** @override */
async _onSubmit ( ... args ) {
if ( this . _tabs [ 0 ] . active === "details" ) this . position . height = "auto" ;
await super . _onSubmit ( ... args ) ;
}
2020-06-24 14:23:26 -04:00
}