forked from GitHub-Mirrors/foundry-sw5e

System main update to be inline with dnd5e 1.1.1 Added active effects to as many sheets as I thought applicable. Please check loot, I made an attempt but it may be broken All .less .css and actor .html updates were made to the old actors. New actors may be broken with this update removed templates\actors\oldActor\parts\actor-effects.html for newer templates\actors\parts\active-effects.html removed module\apps\cast-dialog, templates\apps\cast-cast.html, and templates\items\cast.html. I do not think they are used, I think they were deprecated when powers were treated as items, if not we can add them back in. **NOTE** REQUIRES Foundry 0.7.6
356 lines
11 KiB
JavaScript
356 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);
|
|
|
|
// 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;
|
|
}, {});
|
|
}
|
|
|
|
// 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.edit,
|
|
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);
|
|
}
|
|
}
|