forked from GitHub-Mirrors/foundry-sw5e
Add files via upload
This commit is contained in:
parent
1f3483d825
commit
848158b9e8
3 changed files with 1231 additions and 0 deletions
763
module/actor/sheets/base.js
Normal file
763
module/actor/sheets/base.js
Normal file
|
@ -0,0 +1,763 @@
|
|||
import {TraitSelector} from "../../apps/trait-selector.js";
|
||||
import {ActorSheetFlags} from "../../apps/actor-flags.js";
|
||||
import {SW5E} from '../../config.js';
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet class to do all the D&D5e things!
|
||||
* This sheet is an Abstract layer which is not used.
|
||||
*
|
||||
* @type {ActorSheet}
|
||||
*/
|
||||
export class ActorSheet5e extends ActorSheet {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Track the set of item filters which are applied
|
||||
* @type {Set}
|
||||
*/
|
||||
this._filters = {
|
||||
inventory: new Set(),
|
||||
powerbook: new Set(),
|
||||
features: new Set()
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
scrollY: [
|
||||
".inventory .inventory-list",
|
||||
".features .inventory-list",
|
||||
".powerbook .inventory-list"
|
||||
],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @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",
|
||||
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];
|
||||
}
|
||||
|
||||
// Update skill labels
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
skl.ability = data.actor.data.abilities[skl.ability].label.substring(0, 3);
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
skl.label = CONFIG.SW5E.skills[s];
|
||||
}
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
|
||||
// Prepare owned items
|
||||
this._prepareItems(data);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_prepareTraits(traits) {
|
||||
const map = {
|
||||
"dr": CONFIG.SW5E.damageTypes,
|
||||
"di": CONFIG.SW5E.damageTypes,
|
||||
"dv": CONFIG.SW5E.damageTypes,
|
||||
"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) {
|
||||
const owner = this.actor.owner;
|
||||
const levels = data.data.powers;
|
||||
const powerbook = {};
|
||||
|
||||
// Define some mappings
|
||||
const sections = {
|
||||
"atwill": -20,
|
||||
"innate": -10,
|
||||
"pact": 0.5
|
||||
};
|
||||
|
||||
// Label power slot uses headers
|
||||
const useLabels = {
|
||||
"-20": "-",
|
||||
"-10": "-",
|
||||
"0": "∞"
|
||||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, level={}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner && (i >= 1),
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
powers: [],
|
||||
uses: useLabels[i] || level.value || 0,
|
||||
slots: useLabels[i] || level.max || 0,
|
||||
override: level.override || 0,
|
||||
dataset: {"type": "power", "level": i},
|
||||
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);
|
||||
|
||||
// Structure the powerbook for every level up to the maximum which has a slot
|
||||
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]);
|
||||
}
|
||||
}
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
registerSection("pact", sections.pact, CONFIG.SW5E.powerPreparationModes.pact, levels.pact);
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
|
||||
// Powercasting mode specific headings
|
||||
if ( mode in sections ) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
registerSection(sl, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]);
|
||||
}
|
||||
}
|
||||
|
||||
// Higher-level power headings
|
||||
else if ( !powerbook[s] ) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], 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;
|
||||
return data.preparation.prepared;
|
||||
}
|
||||
|
||||
// Equipment-specific filters
|
||||
if ( filters.has("equipped") ) {
|
||||
if (data.equipped && 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];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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 h4').click(event => this._onItemSummary(event));
|
||||
|
||||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
||||
// Relative updates for numeric fields
|
||||
html.find('input[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('.configure-flags').click(this._onConfigureFlags.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-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
|
||||
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
|
||||
}
|
||||
|
||||
// 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 Dragging
|
||||
let handler = ev => this._onDragItemStart(ev);
|
||||
html.find('li.item').each((i, li) => {
|
||||
if ( li.classList.contains("inventory-header") ) return;
|
||||
li.setAttribute("draggable", true);
|
||||
li.addEventListener("dragstart", handler, false);
|
||||
});
|
||||
|
||||
// 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 click events for the Traits tab button to configure special Character Flags
|
||||
*/
|
||||
_onConfigureFlags(event) {
|
||||
event.preventDefault();
|
||||
new ActorSheetFlags(this.actor).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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 _onDrop (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Get dropped data
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.dataTransfer.getData('text/plain'));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle a polymorph
|
||||
if (data && (data.type === "Actor")) {
|
||||
if (game.user.isGM || (game.settings.get('sw5e', 'allowPolymorphing') && this.actor.owner)) {
|
||||
return this._onDropPolymorph(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Call parent on drop logic
|
||||
return super._onDrop(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle dropping an Actor on the sheet to trigger a Polymorph workflow
|
||||
* @param {DragEvent} event The drop event
|
||||
* @param {Object} data The data transfer
|
||||
* @private
|
||||
*/
|
||||
async _onDropPolymorph(event, data) {
|
||||
|
||||
// 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
|
||||
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, {
|
||||
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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Roll powers through the actor
|
||||
if ( item.data.type === "power" ) {
|
||||
return this.actor.usePower(item, {configureDialog: !event.shiftKey});
|
||||
}
|
||||
|
||||
// Otherwise roll the Item directly
|
||||
else 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: `New ${type.capitalize()}`,
|
||||
type: type,
|
||||
data: duplicate(header.dataset)
|
||||
};
|
||||
delete itemData.data["type"];
|
||||
return this.actor.createOwnedItem(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 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 options = {
|
||||
name: label.getAttribute("for"),
|
||||
title: label.innerText,
|
||||
choices: CONFIG.SW5E[a.dataset.options]
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
313
module/actor/sheets/character.js
Normal file
313
module/actor/sheets/character.js
Normal file
|
@ -0,0 +1,313 @@
|
|||
import { ActorSheet5e } from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the D&D5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export class ActorSheet5eCharacter extends ActorSheet5e {
|
||||
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "character"],
|
||||
width: 672,
|
||||
height: 736
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the correct HTML template path to use for rendering this particular sheet
|
||||
* @type {String}
|
||||
*/
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/character-sheet.html";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const sheetData = super.getData();
|
||||
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Resources
|
||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
}, []);
|
||||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes] = data.items.reduce((arr, item) => {
|
||||
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = item.data.quantity ? item.data.quantity > 1 : false;
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" ) arr[1].push(item);
|
||||
else if ( item.type === "feat" ) arr[2].push(item);
|
||||
else if ( item.type === "class" ) arr[3].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], []]);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter(s => {
|
||||
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
// Organize Inventory
|
||||
let totalWeight = 0;
|
||||
for ( let i of items ) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
||||
inventory[i.type].items.push(i);
|
||||
totalWeight += i.totalWeight;
|
||||
}
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
};
|
||||
for ( let f of feats ) {
|
||||
if ( f.data.activation.type ) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
features.classes.items = classes;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.powerbook = powerbook;
|
||||
data.preparedPowers = nPrepared;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to establish the displayed preparation state for an item
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_prepareItemToggleState(item) {
|
||||
if (item.type === "power") {
|
||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if ( isAlways ) item.toggleClass = "fixed";
|
||||
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
}
|
||||
else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the level and percentage of encumbrance for an Actor.
|
||||
*
|
||||
* Optionally include the weight of carried currency across all denominations by applying the standard rule
|
||||
* from the PHB pg. 143
|
||||
*
|
||||
* @param {Number} totalWeight The cumulative item weight from inventory items
|
||||
* @param {Object} actorData The data object for the Actor being rendered
|
||||
* @return {Object} An object describing the character's encumbrance level
|
||||
* @private
|
||||
*/
|
||||
_computeEncumbrance(totalWeight, actorData) {
|
||||
|
||||
// Encumbrance classes
|
||||
let mod = {
|
||||
tiny: 0.5,
|
||||
sm: 1,
|
||||
med: 1,
|
||||
lg: 2,
|
||||
huge: 4,
|
||||
grg: 8
|
||||
}[actorData.data.traits.size] || 1;
|
||||
|
||||
// Apply Powerful Build feat
|
||||
if ( this.actor.getFlag("sw5e", "powerfulBuild") ) mod = Math.min(mod * 2, 8);
|
||||
|
||||
// Add Currency Weight
|
||||
if ( game.settings.get("sw5e", "currencyWeight") ) {
|
||||
const currency = actorData.data.currency;
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += denom, 0);
|
||||
totalWeight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
|
||||
}
|
||||
|
||||
// Compute Encumbrance percentage
|
||||
const enc = {
|
||||
max: actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod,
|
||||
value: Math.round(totalWeight * 10) / 10,
|
||||
};
|
||||
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
|
||||
enc.encumbered = enc.pct > (2/3);
|
||||
return enc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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) {
|
||||
super.activateListeners(html);
|
||||
if ( !this.options.editable ) return;
|
||||
|
||||
// Inventory Functions
|
||||
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// Item State Toggling
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
|
||||
// Short and Long Rest
|
||||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
|
||||
// Death saving throws
|
||||
html.find('.death-save').click(this._onDeathSave.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a death saving throw for the Character
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onDeathSave(event) {
|
||||
event.preventDefault();
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Handle toggling the state of an Owned Item within the Actor
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a short rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onShortRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.shortRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a long rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onLongRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.longRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events to convert currency to the highest possible denomination
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onConvertCurrency(event) {
|
||||
event.preventDefault();
|
||||
return Dialog.confirm({
|
||||
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
||||
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
||||
yes: () => this.actor.convertCurrency()
|
||||
});
|
||||
}
|
||||
}
|
155
module/actor/sheets/npc.js
Normal file
155
module/actor/sheets/npc.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { ActorSheet5e } from "../sheets/base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the D&D5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export class ActorSheet5eNPC extends ActorSheet5e {
|
||||
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
height: 658
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the correct HTML template path to use for rendering this particular sheet
|
||||
* @type {String}
|
||||
*/
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/npc-sheet.html";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: "Attacks", items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: "Actions", items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "Features", items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: "Inventory", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = item.data.quantity ? item.data.quantity > 1 : false;
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" ) arr[0].push(item);
|
||||
else arr[1].push(item);
|
||||
return arr;
|
||||
}, [[], []]);
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
// Challenge Rating
|
||||
const cr = parseFloat(data.data.details.cr || 0);
|
||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This method is called upon form submission after form data is validated
|
||||
* @param event {Event} The initial triggering submission event
|
||||
* @param formData {Object} The object of validated form data with which to update the object
|
||||
* @private
|
||||
*/
|
||||
_updateObject(event, formData) {
|
||||
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
let cr = formData[crv];
|
||||
cr = crs[cr] || parseFloat(cr);
|
||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Rollable Health Formula
|
||||
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling NPC health values using the provided formula
|
||||
* @param {Event} event The original click event
|
||||
* @private
|
||||
*/
|
||||
_onRollHealthFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
||||
const hp = new Roll(formula).roll().total;
|
||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue