foundry-sw5e/module/actor/sheets/newSheet/base.js
supervj 516e6a0bd1 Created Starship Deployment on actor drop
This lets actors drop onto ships.  It mostly works data wise.  Tokens are stored separately from characters at this point, need to bring that part in from polymorph.
Actors dropped do not appear on character sheet
2021-06-11 16:08:15 -04:00

953 lines
No EOL
30 KiB
JavaScript

import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import {SW5E} from '../../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
forcePowerbook: new Set(),
techPowerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .group-list",
".features .group-list",
".force-powerbook .group-list",
".tech-powerbook .group-list",
".effects .effects-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Basic data
let isOwner = this.entity.owner;
const data = {
owner: isOwner,
limited: this.entity.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc",
isStarship: this.entity.data.type === "starship",
isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E,
};
// The Actor and its Items
data.actor = duplicate(this.actor.data);
data.items = this.actor.items.map(i => {
i.data.labels = i.labels;
return i.data;
});
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
if (data.actor.type === "starship") {
skl.label = CONFIG.SW5E.starshipSkills[s];
}else{
skl.label = CONFIG.SW5E.skills[s];
}
}
}
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
// Senses
data.senses = this._getSenses(data.actor);
// Update traits
this._prepareTraits(data.actor.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
// Return data to the sheet
return data
}
/* -------------------------------------------- */
/**
* Prepare the display of movement speed data for the Actor*
* @param {object} actorData The Actor data being prepared.
* @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData, largestPrimary=false) {
const movement = actorData.data.attributes.movement || {};
// Prepare an array of available movement speeds
let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
]
if ( largestPrimary ) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
}
// Filter and sort speeds on their values
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary
if ( largestPrimary ) {
let primary = speeds.shift();
return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map(s => s[1]).join(", ")
}
}
// Case 2: Walk as primary
else {
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}
}
}
/* -------------------------------------------- */
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0
if ( v === 0 ) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if ( !!senses.special ) tags["special"] = senses.special;
return tags;
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
* @private
*/
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies
};
for ( let [t, choices] of Object.entries(map) ) {
const trait = traits[t];
if ( !trait ) continue;
let values = [];
if ( trait.value ) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
obj[t] = choices[t];
return obj;
}, {});
// Add custom entry
if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared
* @private
*/
_preparePowerbook(data, powers, school) {
const owner = this.actor.owner;
const levels = data.data.powers;
const powerbook = {};
// Define some mappings
const sections = {
"atwill": -20,
"innate": -10,
};
// Label power slot uses headers
const useLabels = {
"-20": "-",
"-10": "-",
"0": "∞"
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1),
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
override: override || 0,
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school},
prop: sl
};
};
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max;
const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if ( maxLevel > 0 ) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
}
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if ( mode in sections ) {
s = sections[mode];
if ( !powerbook[s] ){
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
prepMode: mode,
value: l.value,
max: l.max,
override: l.override
});
}
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if ( !powerbook[s] ) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
// Add the power to the relevant heading
powerbook[s].powers.push(power);
});
// Sort the powerbook by section level
const sorted = Object.values(powerbook);
sorted.sort((a, b) => a.order - b.order);
return sorted;
}
/* -------------------------------------------- */
/**
* Determine whether an Owned Item will be shown based on the current set of filters
* @return {boolean}
* @private
*/
_filterItems(items, filters) {
return items.filter(item => {
const data = item.data;
// Action usage
for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) {
if ((data.activation && (data.activation.type !== f))) return false;
}
}
// Power-specific filters
if ( filters.has("ritual") ) {
if (data.components.ritual !== true) return false;
}
if ( filters.has("concentration") ) {
if (data.components.concentration !== true) return false;
}
if ( filters.has("prepared") ) {
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
if ( this.actor.data.type === "npc" ) return true;
if ( this.actor.data.type === "starship" ) return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if ( filters.has("equipped") ) {
if ( data.equipped !== true ) return false;
}
return true;
});
}
/* -------------------------------------------- */
/**
* Get the font-awesome icon used to display a certain level of skill proficiency
* @private
*/
_getProficiencyIcon(level) {
const icons = {
0: '<i class="far fa-circle"></i>',
0.5: '<i class="fas fa-adjust"></i>',
1: '<i class="fas fa-check"></i>',
2: '<i class="fas fa-check-double"></i>'
};
return icons[level] || icons[0];
}
/* -------------------------------------------- */
/* Event Listeners and Handlers
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
// Editable Only Listeners
if ( this.isEditable ) {
// Input focus and update
const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.config-button').click(this._onConfigMenu.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find('.item-collapse').click(this._onItemCollapse.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this));
html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this));
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
}
// Owner Only Listeners
if ( this.actor.owner ) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
else {
html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
}
// Handle default listeners last so system listeners are triggered first
super.activateListeners(html);
}
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for ( let li of filters ) {
if ( set.has(li.dataset.filter) ) li.classList.add("active");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/**
* Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
* @param event
* @private
*/
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if ( ["+", "-"].includes(value[0]) ) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if ( value[0] === "=" ) {
input.value = value.slice(1);
}
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
switch ( button.dataset.action ) {
case "movement":
new ActorMovementConfig(this.object).render(true);
break;
case "flags":
new ActorSheetFlags(this.object).render(true);
break;
case "senses":
new ActorSensesConfig(this.object).render(true);
break;
}
}
/* -------------------------------------------- */
/**
* Handle cycling proficiency in a Skill
* @param {Event} event A click or contextmenu event which triggered the handler
* @private
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
// Get the current level and the array of levels
const level = parseFloat(field.val());
const levels = [0, 1, 0.5, 2];
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
this._onSubmit(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.data.type === "starship") || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if ( !sourceActor ) return;
// Define a function to record polymorph settings for future use
const rememberOptions = html => {
const options = {};
html.find('input').each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings);
return settings;
};
// Create and render the Dialog
if (this.actor.data.type === "starship") {
return new Dialog({
title: game.i18n.localize('SW5E.DeploymentPromptTitle') + " " + sourceActor.data.name + " into " + this.actor.data.name,
content: {
i18n: SW5E.deploymentTypes,
isToken: this.actor.isToken
},
default: 'accept',
buttons: {
deploy: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.DeploymentAcceptSettings'),
callback: html => this.actor.deployInto(sourceActor, rememberOptions(html))
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel')
}
}
}, {
classes: ['dialog', 'sw5e'],
width: 600,
template: 'systems/sw5e/templates/apps/deployment-prompt.html'
}).render(true);
} else {
return new Dialog({
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
content: {
options: game.settings.get('sw5e', 'polymorphSettings'),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: 'accept',
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
mergeSaves: true,
mergeSkills: true,
transformTokens: rememberOptions(html).transformTokens
})
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'),
callback: html => this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel')
}
}
}, {
classes: ['dialog', 'sw5e'],
width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
}).render(true);
}
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
// Ignore certain statuses
if ( itemData.data ) {
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
}
// Create the owned item as normal
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Handle enabling editing for a power slot override value
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride (event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
const input = document.createElement("INPUT");
input.type = "text";
input.name = `data.powers.${level}.override`;
input.value = override;
input.placeholder = span.dataset.slots;
input.dataset.dtype = "Number";
// Replace the HTML
const parent = span.parentElement;
parent.removeChild(span);
parent.appendChild(input);
}
/* -------------------------------------------- */
/**
* Change the uses amount of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
return item.roll();
}
/* -------------------------------------------- */
/**
* Handle attempting to recharge an item usage by rolling a recharge check
* @param {Event} event The originating click event
* @private
*/
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
return item.rollRecharge();
};
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.owner});
// Toggle summary
if ( li.hasClass("expanded") ) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
}
li.toggleClass("expanded");
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {Event} event The originating click event
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type,
data: duplicate(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
}
/* -------------------------------------------- */
/**
* Handle editing an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.getOwnedItem(li.dataset.itemId);
item.sheet.render(true);
}
/* -------------------------------------------- */
/**
* Handle deleting an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
this.actor.deleteOwnedItem(li.dataset.itemId);
}
/**
* Handle collapsing a Feature row on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onItemCollapse(event) {
event.preventDefault();
event.currentTarget.classList.toggle("active");
const li = event.currentTarget.closest("li");
const content = li.querySelector(".content");
if (content.style.display === "none") {
content.style.display = "block";
} else {
content.style.display = "none";
}
}
/**
* Handle incrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onIncrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character")
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.getOwnedItem(itemId);
let levels = item.data.data.levels;
const update = {_id: item._id, data: {levels: (levels + 1) }};
actor.updateOwnedItem(update)
}
/**
* Handle decrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onDecrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character")
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.getOwnedItem(itemId);
let levels = item.data.data.levels;
const update = {_id: item._id, data: {levels: (levels - 1) }};
actor.updateOwnedItem(update)
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
* @private
*/
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
/**
* Handle rolling a Skill check
* @param {Event} event The originating click event
* @private
*/
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
/**
* Handle toggling Ability score proficiency level
* @param {Event} event The originating click event
* @private
*/
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
/**
* Handle toggling of filters to display a different set of owned items
* @param {Event} event The click event which triggered the toggle
* @private
*/
_onToggleFilter(event) {
event.preventDefault();
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
this.render();
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onTraitSelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
new TraitSelector(this.actor, options).render(true)
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation",
icon: "fas fa-backward",
onclick: ev => this.actor.revertOriginalForm()
});
return buttons;
}
}