foundry-sw5e/module/item/sheet.js
supervj 9c6bd3873e Update Core 1.2.2
Update core to 1.2.2.  Sheets are broken
2021-01-18 23:49:04 -05:00

359 lines
11 KiB
JavaScript

import TraitSelector from "../apps/trait-selector.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
/**
* Override and extend the core ItemSheet implementation to handle specific item types
* @extends {ItemSheet}
*/
export default class ItemSheet5e extends ItemSheet {
constructor(...args) {
super(...args);
// Expand the default size of the class sheet
if ( this.object.data.type === "class" ) {
this.options.width = this.position.width = 600;
this.options.height = this.position.height = 680;
}
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
width: 560,
height: 400,
classes: ["sw5e", "sheet", "item"],
resizable: true,
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 */
async getData(options) {
const data = super.getData(options);
data.labels = this.item.labels;
data.config = CONFIG.SW5E;
// Item Type, Status, and Details
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
data.itemStatus = this._getItemStatus(data.item);
data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = data.item.data.hasOwnProperty("quantity");
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Details
data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
// Original maximum uses formula
if ( this.item._data.data?.uses?.max ) data.data.uses.max = this.item._data.data.uses.max;
// Vehicles
data.isCrewed = data.item.data.activation?.type === 'crew';
data.isMountable = this._isItemMountable(data.item);
// Prepare Active Effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
return data;
}
/* -------------------------------------------- */
/**
* 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;
}, {[item._id]: `${item.name} (${item.data.quantity})`});
}
// 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) => {
// Limited-use items
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;
}
// Recharging items
const recharge = i.data.data.recharge || {};
if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
return obj;
}, {})
}
else return {};
}
/* -------------------------------------------- */
/**
* 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) ) {
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
}
else if ( item.type === "tool" ) {
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
}
}
/* -------------------------------------------- */
/**
* 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,
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
)
}
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);
}
else if ( item.type === "species" ) {
//props.push(labels.species);
}
else if ( item.type === "archetype" ) {
//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);
}
// 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);
}
/* -------------------------------------------- */
/**
* 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');
}
/* -------------------------------------------- */
/** @override */
setPosition(position={}) {
if ( !(this._minimized || position.height) ) {
position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height;
}
return super.setPosition(position);
}
/* -------------------------------------------- */
/* Form Submission */
/* -------------------------------------------- */
/** @override */
_getSubmitData(updateData={}) {
// 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);
// Handle Damage array
const damage = data.data?.damage;
if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
// Return the flattened submission data
return flattenObject(data);
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
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)
});
}
}
/* -------------------------------------------- */
/**
* 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, {
name: a.dataset.target,
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)
}
/* -------------------------------------------- */
/** @override */
async _onSubmit(...args) {
if ( this._tabs[0].active === "details" ) this.position.height = "auto";
await super._onSubmit(...args);
}
}