forked from GitHub-Mirrors/foundry-sw5e
Spot the link / entityClass error!
This commit is contained in:
parent
c9a9f75a5d
commit
5f5a145626
20 changed files with 1039 additions and 5635 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,920 +0,0 @@
|
|||
import { Dice5e } from "../dice.js";
|
||||
import { AbilityUseDialog } from "../apps/ability-use-dialog.js";
|
||||
import { AbilityTemplate } from "../pixi/ability-template.js";
|
||||
|
||||
/**
|
||||
* Override and extend the basic :class:`Item` implementation
|
||||
*/
|
||||
export class Item5e extends Item {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Item Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine which ability score modifier is used by this item
|
||||
* @type {string|null}
|
||||
*/
|
||||
get abilityMod() {
|
||||
const itemData = this.data.data;
|
||||
if (!("ability" in itemData)) return null;
|
||||
|
||||
// Case 1 - defined directly by the item
|
||||
if ( itemData.ability ) return itemData.ability;
|
||||
|
||||
// Case 2 - inferred from a parent actor
|
||||
else if ( this.actor ) {
|
||||
const actorData = this.actor.data.data;
|
||||
if ( this.data.type === "power" ) return actorData.attributes.powercasting || "int";
|
||||
else if ( this.data.type === "tool" ) return "int";
|
||||
else return "str";
|
||||
}
|
||||
|
||||
// Case 3 - unknown
|
||||
return null
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the Item implement an attack roll as part of its usage
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasAttack() {
|
||||
return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the Item implement a damage roll as part of its usage
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasDamage() {
|
||||
return !!(this.data.data.damage && this.data.data.damage.parts.length);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the Item implement a versatile damage roll as part of its usage
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isVersatile() {
|
||||
return !!(this.hasDamage && this.data.data.damage.versatile);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the item provide an amount of healing instead of conventional damage?
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isHealing() {
|
||||
return (this.data.data.actionType === "heal") && this.data.data.damage.parts.length;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the Item implement a saving throw as part of its usage
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasSave() {
|
||||
return !!(this.data.data.save && this.data.data.save.ability);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the Item have a target
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasTarget() {
|
||||
const target = this.data.data.target;
|
||||
return target && !["none",""].includes(target.type);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Does the Item have an area of effect target
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasAreaTarget() {
|
||||
const target = this.data.data.target;
|
||||
return target && (target.type in CONFIG.SW5E.areaTargetTypes);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A flag for whether this Item is limited in it's ability to be used by charges or by recharge.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasLimitedUses() {
|
||||
let chg = this.data.data.recharge || {};
|
||||
let uses = this.data.data.uses || {};
|
||||
return !!chg.value || (!!uses.per && (uses.max > 0));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Data Preparation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Augment the basic Item data model with additional dynamic data.
|
||||
*/
|
||||
prepareData() {
|
||||
super.prepareData();
|
||||
|
||||
// Get the Item's data
|
||||
const itemData = this.data;
|
||||
const actorData = this.actor ? this.actor.data : {};
|
||||
const data = itemData.data;
|
||||
const C = CONFIG.SW5E;
|
||||
const labels = {};
|
||||
|
||||
// Classes
|
||||
if ( itemData.type === "class" ) {
|
||||
data.levels = Math.clamped(data.levels, 1, 20);
|
||||
}
|
||||
|
||||
// Power Level, School, and Components
|
||||
if ( itemData.type === "power" ) {
|
||||
labels.level = C.powerLevels[data.level];
|
||||
labels.school = C.powerSchools[data.school];
|
||||
labels.components = Object.entries(data.components).reduce((arr, c) => {
|
||||
if ( c[1] !== true ) return arr;
|
||||
arr.push(c[0].titleCase().slice(0, 1));
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// Feat Items
|
||||
else if ( itemData.type === "feat" ) {
|
||||
const act = data.activation;
|
||||
if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = "Legendary Action";
|
||||
else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = "Lair Action";
|
||||
else if ( act && act.type ) labels.featType = data.damage.length ? "Attack" : "Action";
|
||||
else labels.featType = "Passive";
|
||||
}
|
||||
|
||||
// Species Items
|
||||
else if ( itemData.type === "species" ) {
|
||||
|
||||
}
|
||||
// Equipment Items
|
||||
else if ( itemData.type === "equipment" ) {
|
||||
labels.armor = data.armor.value ? `${data.armor.value} AC` : "";
|
||||
}
|
||||
|
||||
// Activated Items
|
||||
if ( data.hasOwnProperty("activation") ) {
|
||||
|
||||
// Ability Activation Label
|
||||
let act = data.activation || {};
|
||||
if ( act ) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" ");
|
||||
|
||||
// Target Label
|
||||
let tgt = data.target || {};
|
||||
if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null;
|
||||
if (["none", "self"].includes(tgt.type)) {
|
||||
tgt.value = null;
|
||||
tgt.units = null;
|
||||
}
|
||||
labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" ");
|
||||
|
||||
// Range Label
|
||||
let rng = data.range || {};
|
||||
if (["none", "touch", "self"].includes(rng.units) || (rng.value === 0)) {
|
||||
rng.value = null;
|
||||
rng.long = null;
|
||||
}
|
||||
labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" ");
|
||||
|
||||
// Duration Label
|
||||
let dur = data.duration || {};
|
||||
if (["inst", "perm"].includes(dur.units)) dur.value = null;
|
||||
labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" ");
|
||||
|
||||
// Recharge Label
|
||||
let chg = data.recharge || {};
|
||||
labels.recharge = `Recharge [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`;
|
||||
}
|
||||
|
||||
// Item Actions
|
||||
if ( data.hasOwnProperty("actionType") ) {
|
||||
|
||||
// Save DC
|
||||
let save = data.save || {};
|
||||
if ( !save.ability ) save.dc = null;
|
||||
else if ( this.isOwned ) { // Actor owned items
|
||||
if ( save.scaling === "power" ) save.dc = actorData.data.attributes.powerdc;
|
||||
else if ( save.scaling !== "flat" ) save.dc = this.actor.getPowerDC(save.scaling);
|
||||
} else { // Un-owned items
|
||||
if ( save.scaling !== "flat" ) save.dc = null;
|
||||
}
|
||||
labels.save = save.ability ? `DC ${save.dc || ""} ${C.abilities[save.ability]}` : "";
|
||||
|
||||
// Damage
|
||||
let dam = data.damage || {};
|
||||
if ( dam.parts ) {
|
||||
labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
|
||||
labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
// Assign labels
|
||||
this.labels = labels;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options
|
||||
* @return {Promise}
|
||||
*/
|
||||
async roll({configureDialog=true}={}) {
|
||||
|
||||
// Basic template rendering data
|
||||
const token = this.actor.token;
|
||||
const templateData = {
|
||||
actor: this.actor,
|
||||
tokenId: token ? `${token.scene._id}.${token.id}` : null,
|
||||
item: this.data,
|
||||
data: this.getChatData(),
|
||||
labels: this.labels,
|
||||
hasAttack: this.hasAttack,
|
||||
isHealing: this.isHealing,
|
||||
hasDamage: this.hasDamage,
|
||||
isVersatile: this.isVersatile,
|
||||
isPower: this.data.type === "power",
|
||||
hasSave: this.hasSave,
|
||||
hasAreaTarget: this.hasAreaTarget
|
||||
};
|
||||
|
||||
// For feature items, optionally show an ability usage dialog
|
||||
if (this.data.type === "feat") {
|
||||
let configured = await this._rollFeat(configureDialog);
|
||||
if ( configured === false ) return;
|
||||
}
|
||||
|
||||
// Render the chat card template
|
||||
const templateType = ["tool", "consumable"].includes(this.data.type) ? this.data.type : "item";
|
||||
const template = `systems/sw5e/templates/chat/${templateType}-card.html`;
|
||||
const html = await renderTemplate(template, templateData);
|
||||
|
||||
// Basic chat message data
|
||||
const chatData = {
|
||||
user: game.user._id,
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
|
||||
content: html,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle default roll mode
|
||||
let rollMode = game.settings.get("core", "rollMode");
|
||||
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperIDs("GM");
|
||||
if ( rollMode === "blindroll" ) chatData["blind"] = true;
|
||||
|
||||
// Create the chat message
|
||||
return ChatMessage.create(chatData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Additional rolling steps when rolling a feat-type item
|
||||
* @private
|
||||
* @return {boolean} whether the roll should be prevented
|
||||
*/
|
||||
async _rollFeat(configureDialog) {
|
||||
if ( this.data.type !== "feat" ) throw new Error("Wrong Item type");
|
||||
|
||||
// Configure whether to consume a limited use or to place a template
|
||||
const usesRecharge = !!this.data.data.recharge.value;
|
||||
const uses = this.data.data.uses;
|
||||
let usesCharges = !!uses.per && (uses.max > 0);
|
||||
let placeTemplate = false;
|
||||
let consume = usesRecharge || usesCharges;
|
||||
|
||||
// Determine whether the feat uses charges
|
||||
configureDialog = configureDialog && (consume || this.hasAreaTarget);
|
||||
if ( configureDialog ) {
|
||||
const usage = await AbilityUseDialog.create(this);
|
||||
if ( usage === null ) return false;
|
||||
consume = Boolean(usage.get("consume"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
const current = getProperty(this.data, "data.uses.value") || 0;
|
||||
if ( consume && usesRecharge ) {
|
||||
await this.update({"data.recharge.charged": false});
|
||||
}
|
||||
else if ( consume && usesCharges ) {
|
||||
await this.update({"data.uses.value": Math.max(current - 1, 0)});
|
||||
}
|
||||
|
||||
// Maybe initiate template placement workflow
|
||||
if ( this.hasAreaTarget && placeTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(this);
|
||||
if ( template ) template.drawPreview(event);
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Chat Cards */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare an object of chat data used to display a card for the Item in the chat log
|
||||
* @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function
|
||||
* @return {Object} An object of chat data to render
|
||||
*/
|
||||
getChatData(htmlOptions) {
|
||||
const data = duplicate(this.data.data);
|
||||
const labels = this.labels;
|
||||
|
||||
// Rich text description
|
||||
data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions);
|
||||
|
||||
// Item type specific properties
|
||||
const props = [];
|
||||
const fn = this[`_${this.data.type}ChatData`];
|
||||
if ( fn ) fn.bind(this)(data, labels, props);
|
||||
|
||||
// General equipment properties
|
||||
if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) {
|
||||
props.push(
|
||||
data.equipped ? "Equipped" : "Not Equipped",
|
||||
data.proficient ? "Proficient": "Not Proficient",
|
||||
);
|
||||
}
|
||||
|
||||
// Ability activation properties
|
||||
if ( data.hasOwnProperty("activation") ) {
|
||||
props.push(
|
||||
labels.target,
|
||||
labels.activation,
|
||||
labels.range,
|
||||
labels.duration
|
||||
);
|
||||
}
|
||||
|
||||
// Filter properties and return
|
||||
data.properties = props.filter(p => !!p);
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for equipment type items
|
||||
* @private
|
||||
*/
|
||||
_equipmentChatData(data, labels, props) {
|
||||
props.push(
|
||||
CONFIG.SW5E.equipmentTypes[data.armor.type],
|
||||
labels.armor || null,
|
||||
data.stealth.value ? "Stealth Disadvantage" : null,
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for weapon type items
|
||||
* @private
|
||||
*/
|
||||
_weaponChatData(data, labels, props) {
|
||||
props.push(
|
||||
CONFIG.SW5E.weaponTypes[data.weaponType],
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for consumable type items
|
||||
* @private
|
||||
*/
|
||||
_consumableChatData(data, labels, props) {
|
||||
props.push(
|
||||
CONFIG.SW5E.consumableTypes[data.consumableType],
|
||||
data.uses.value + "/" + data.uses.max + " Charges"
|
||||
);
|
||||
data.hasCharges = data.uses.value >= 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for tool type items
|
||||
* @private
|
||||
*/
|
||||
_toolChatData(data, labels, props) {
|
||||
props.push(
|
||||
CONFIG.SW5E.abilities[data.ability] || null,
|
||||
CONFIG.SW5E.proficiencyLevels[data.proficient || 0]
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for tool type items
|
||||
* @private
|
||||
*/
|
||||
_lootChatData(data, labels, props) {
|
||||
props.push(
|
||||
"Loot",
|
||||
data.weight ? data.weight + " lbs." : null
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render a chat card for Power type data
|
||||
* @return {Object}
|
||||
* @private
|
||||
*/
|
||||
_powerChatData(data, labels, props) {
|
||||
props.push(
|
||||
labels.level,
|
||||
labels.components,
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for items of the "Feat" type
|
||||
* @private
|
||||
*/
|
||||
_featChatData(data, labels, props) {
|
||||
props.push(data.requirements);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Item Rolls - Attack, Damage, Saves, Checks */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Place an attack roll using an item (weapon, feat, power, or equipment)
|
||||
* Rely upon the Dice5e.d20Roll logic for the core implementation
|
||||
*
|
||||
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
rollAttack(options={}) {
|
||||
const itemData = this.data.data;
|
||||
const actorData = this.actor.data.data;
|
||||
const flags = this.actor.data.flags.sw5e || {};
|
||||
if ( !this.hasAttack ) {
|
||||
throw new Error("You may not place an Attack Roll with this Item.");
|
||||
}
|
||||
const rollData = this.getRollData();
|
||||
|
||||
// Define Roll bonuses
|
||||
const parts = [`@mod`];
|
||||
if ( (this.data.type !== "weapon") || itemData.proficient ) {
|
||||
parts.push("@prof");
|
||||
}
|
||||
|
||||
// Attack Bonus
|
||||
const actorBonus = actorData.bonuses[itemData.actionType] || {};
|
||||
if ( itemData.attackBonus || actorBonus.attack ) {
|
||||
parts.push("@atk");
|
||||
rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + ");
|
||||
}
|
||||
|
||||
// Compose roll options
|
||||
const rollConfig = {
|
||||
event: options.event,
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
data: rollData,
|
||||
title: `${this.name} - Attack Roll`,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
top: options.event ? options.event.clientY - 80 : null,
|
||||
left: window.innerWidth - 710
|
||||
}
|
||||
};
|
||||
|
||||
// Expanded weapon critical threshold
|
||||
if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) {
|
||||
rollConfig.critical = parseInt(flags.weaponCriticalThreshold);
|
||||
}
|
||||
|
||||
// Elven Accuracy
|
||||
if ( ["weapon", "power"].includes(this.data.type) ) {
|
||||
if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) {
|
||||
rollConfig.elvenAccuracy = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply Halfling Lucky
|
||||
if ( flags.halflingLucky ) rollConfig.halflingLucky = true;
|
||||
|
||||
// Invoke the d20 roll helper
|
||||
return Dice5e.d20Roll(rollConfig);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Place a damage roll using an item (weapon, feat, power, or equipment)
|
||||
* Rely upon the Dice5e.damageRoll logic for the core implementation
|
||||
*
|
||||
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
rollDamage({event, powerLevel=null, versatile=false}={}) {
|
||||
const itemData = this.data.data;
|
||||
const actorData = this.actor.data.data;
|
||||
if ( !this.hasDamage ) {
|
||||
throw new Error("You may not make a Damage Roll with this Item.");
|
||||
}
|
||||
const rollData = this.getRollData();
|
||||
if ( powerLevel ) rollData.item.level = powerLevel;
|
||||
|
||||
// Define Roll parts
|
||||
const parts = itemData.damage.parts.map(d => d[0]);
|
||||
if ( versatile && itemData.damage.versatile ) parts[0] = itemData.damage.versatile;
|
||||
if ( (this.data.type === "power") ) {
|
||||
if ( (itemData.scaling.mode === "atwill") ) {
|
||||
const lvl = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
|
||||
this._scaleAtWillDamage(parts, lvl, itemData.scaling.formula );
|
||||
} else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) {
|
||||
this._scalePowerDamage(parts, itemData.level, powerLevel, itemData.scaling.formula );
|
||||
}
|
||||
}
|
||||
|
||||
// Define Roll Data
|
||||
const actorBonus = actorData.bonuses[itemData.actionType] || {};
|
||||
if ( actorBonus.damage && parseInt(actorBonus.damage) !== 0 ) {
|
||||
parts.push("@dmg");
|
||||
rollData["dmg"] = actorBonus.damage;
|
||||
}
|
||||
|
||||
// Call the roll helper utility
|
||||
const title = `${this.name} - Damage Roll`;
|
||||
const flavor = this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title;
|
||||
return Dice5e.damageRoll({
|
||||
event: event,
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
data: rollData,
|
||||
title: title,
|
||||
flavor: flavor,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
top: event ? event.clientY - 80 : null,
|
||||
left: window.innerWidth - 710
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Adjust an at-will damage formula to scale it for higher level characters and monsters
|
||||
* @private
|
||||
*/
|
||||
_scaleAtWillDamage(parts, level, scale) {
|
||||
const add = Math.floor((level + 1) / 6);
|
||||
if ( add === 0 ) return;
|
||||
if ( scale && (scale !== parts[0]) ) {
|
||||
parts[0] = parts[0] + " + " + scale.replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${add}d${d}`);
|
||||
} else {
|
||||
parts[0] = parts[0].replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${parseInt(nd)+add}d${d}`);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Adjust the power damage formula to scale it for power level up-casting
|
||||
* @param {Array} parts The original damage parts
|
||||
* @param {number} baseLevel The default power level
|
||||
* @param {number} powerLevel The casted power level
|
||||
* @param {string} formula The scaling formula
|
||||
* @private
|
||||
*/
|
||||
_scalePowerDamage(parts, baseLevel, powerLevel, formula) {
|
||||
const upcastLevels = Math.max(powerLevel - baseLevel, 0);
|
||||
if ( upcastLevels === 0 ) return parts;
|
||||
const bonus = new Roll(formula).alter(0, upcastLevels);
|
||||
parts.push(bonus.formula);
|
||||
return parts;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Place an attack roll using an item (weapon, feat, power, or equipment)
|
||||
* Rely upon the Dice5e.d20Roll logic for the core implementation
|
||||
*
|
||||
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
async rollFormula(options={}) {
|
||||
if ( !this.data.data.formula ) {
|
||||
throw new Error("This Item does not have a formula to roll!");
|
||||
}
|
||||
|
||||
// Define Roll Data
|
||||
const rollData = this.getRollData();
|
||||
const title = `${this.name} - Other Formula`;
|
||||
|
||||
// Invoke the roll and submit it to chat
|
||||
const roll = new Roll(rollData.item.formula, rollData).roll();
|
||||
roll.toMessage({
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
flavor: this.data.data.chatFlavor || title,
|
||||
rollMode: game.settings.get("core", "rollMode")
|
||||
});
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Use a consumable item, deducting from the quantity or charges of the item.
|
||||
*
|
||||
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance or null
|
||||
*/
|
||||
async rollConsumable(options={}) {
|
||||
const itemData = this.data.data;
|
||||
|
||||
// Dispatch a damage roll
|
||||
let roll = null;
|
||||
if ( itemData.damage.parts.length ) {
|
||||
roll = await this.rollDamage(options);
|
||||
}
|
||||
|
||||
// Dispatch an other formula
|
||||
if ( itemData.formula ) {
|
||||
roll = await this.rollFormula(options);
|
||||
}
|
||||
|
||||
// Deduct consumed charges from the item
|
||||
if ( itemData.uses.autoUse ) {
|
||||
let q = itemData.quantity;
|
||||
let c = itemData.uses.value;
|
||||
|
||||
// Deduct an item quantity
|
||||
if ( c <= 1 && q > 1 ) {
|
||||
await this.update({
|
||||
'data.quantity': Math.max(q - 1, 0),
|
||||
'data.uses.value': itemData.uses.max
|
||||
});
|
||||
}
|
||||
|
||||
// Optionally destroy the item
|
||||
else if ( c <= 1 && q <= 1 && itemData.uses.autoDestroy ) {
|
||||
await this.actor.deleteOwnedItem(this.id);
|
||||
}
|
||||
|
||||
// Deduct the remaining charges
|
||||
else {
|
||||
await this.update({'data.uses.value': Math.max(c - 1, 0)});
|
||||
}
|
||||
}
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Perform an ability recharge test for an item which uses the d6 recharge mechanic
|
||||
* @prarm {Object} options
|
||||
*
|
||||
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
async rollRecharge(options={}) {
|
||||
const data = this.data.data;
|
||||
if ( !data.recharge.value ) return;
|
||||
|
||||
// Roll the check
|
||||
const roll = new Roll("1d6").roll();
|
||||
const success = roll.total >= parseInt(data.recharge.value);
|
||||
|
||||
// Display a Chat Message
|
||||
const promises = [roll.toMessage({
|
||||
flavor: `${this.name} recharge check - ${success ? "success!" : "failure!"}`,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
|
||||
})];
|
||||
|
||||
// Update the Item data
|
||||
if ( success ) promises.push(this.update({"data.recharge.charged": true}));
|
||||
return Promise.all(promises).then(() => roll);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Roll a Tool Check
|
||||
* Rely upon the Dice5e.d20Roll logic for the core implementation
|
||||
*
|
||||
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
rollToolCheck(options={}) {
|
||||
if ( this.type !== "tool" ) throw "Wrong item type!";
|
||||
|
||||
// Prepare roll data
|
||||
let rollData = this.getRollData();
|
||||
const parts = [`@mod`, "@prof"];
|
||||
const title = `${this.name} - Tool Check`;
|
||||
|
||||
// Call the roll helper utility
|
||||
return Dice5e.d20Roll({
|
||||
event: options.event,
|
||||
parts: parts,
|
||||
data: rollData,
|
||||
template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
|
||||
title: title,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
flavor: `${this.name} - Tool Check`,
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
top: options.event ? options.event.clientY - 80 : null,
|
||||
left: window.innerWidth - 710,
|
||||
},
|
||||
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare a data object which is passed to any Roll formulas which are created related to this Item
|
||||
* @private
|
||||
*/
|
||||
getRollData() {
|
||||
if ( !this.actor ) return null;
|
||||
const rollData = this.actor.getRollData();
|
||||
rollData.item = duplicate(this.data.data);
|
||||
|
||||
// Include an ability score modifier if one exists
|
||||
const abl = this.abilityMod;
|
||||
if ( abl ) {
|
||||
const ability = rollData.abilities[abl];
|
||||
rollData["mod"] = ability.mod || 0;
|
||||
}
|
||||
|
||||
// Include a proficiency score
|
||||
const prof = "proficient" in rollData.item ? (rollData.item.proficient || 0) : 1;
|
||||
rollData["prof"] = Math.floor(prof * rollData.attributes.prof);
|
||||
return rollData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Chat Message Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static chatListeners(html) {
|
||||
html.on('click', '.card-buttons button', this._onChatCardAction.bind(this));
|
||||
html.on('click', '.item-name', this._onChatCardToggleContent.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle execution of a chat card action via a click event on one of the card buttons
|
||||
* @param {Event} event The originating click event
|
||||
* @returns {Promise} A promise which resolves once the handler workflow is complete
|
||||
* @private
|
||||
*/
|
||||
static async _onChatCardAction(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Extract card data
|
||||
const button = event.currentTarget;
|
||||
button.disabled = true;
|
||||
const card = button.closest(".chat-card");
|
||||
const messageId = card.closest(".message").dataset.messageId;
|
||||
const message = game.messages.get(messageId);
|
||||
const action = button.dataset.action;
|
||||
|
||||
// Validate permission to proceed with the roll
|
||||
const isTargetted = action === "save";
|
||||
if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return;
|
||||
|
||||
// Get the Actor from a synthetic Token
|
||||
const actor = this._getChatCardActor(card);
|
||||
if ( !actor ) return;
|
||||
|
||||
// Get the Item
|
||||
const item = actor.getOwnedItem(card.dataset.itemId);
|
||||
if ( !item ) {
|
||||
return ui.notifications.error(`The requested item ${card.dataset.itemId} no longer exists on Actor ${actor.name}`)
|
||||
}
|
||||
const powerLevel = parseInt(card.dataset.powerLevel) || null;
|
||||
|
||||
// Get card targets
|
||||
let targets = [];
|
||||
if ( isTargetted ) {
|
||||
targets = this._getChatCardTargets(card);
|
||||
if ( !targets.length ) {
|
||||
ui.notifications.warn(`You must have one or more controlled Tokens in order to use this option.`);
|
||||
return button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Attack and Damage Rolls
|
||||
if ( action === "attack" ) await item.rollAttack({event});
|
||||
else if ( action === "damage" ) await item.rollDamage({event, powerLevel});
|
||||
else if ( action === "versatile" ) await item.rollDamage({event, powerLevel, versatile: true});
|
||||
else if ( action === "formula" ) await item.rollFormula({event});
|
||||
|
||||
// Saving Throws for card targets
|
||||
else if ( action === "save" ) {
|
||||
for ( let t of targets ) {
|
||||
await t.rollAbilitySave(button.dataset.ability, {event});
|
||||
}
|
||||
}
|
||||
|
||||
// Consumable usage
|
||||
else if ( action === "consume" ) await item.rollConsumable({event});
|
||||
|
||||
// Tool usage
|
||||
else if ( action === "toolCheck" ) await item.rollToolCheck({event});
|
||||
|
||||
// Power Template Creation
|
||||
else if ( action === "placeTemplate") {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview(event);
|
||||
}
|
||||
|
||||
// Re-enable the button
|
||||
button.disabled = false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling the visibility of chat card content when the name is clicked
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
static _onChatCardToggleContent(event) {
|
||||
event.preventDefault();
|
||||
const header = event.currentTarget;
|
||||
const card = header.closest(".chat-card");
|
||||
const content = card.querySelector(".card-content");
|
||||
content.style.display = content.style.display === "none" ? "block" : "none";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the Actor which is the author of a chat card
|
||||
* @param {HTMLElement} card The chat card being used
|
||||
* @return {Actor|null} The Actor entity or null
|
||||
* @private
|
||||
*/
|
||||
static _getChatCardActor(card) {
|
||||
|
||||
// Case 1 - a synthetic actor from a Token
|
||||
const tokenKey = card.dataset.tokenId;
|
||||
if (tokenKey) {
|
||||
const [sceneId, tokenId] = tokenKey.split(".");
|
||||
const scene = game.scenes.get(sceneId);
|
||||
if (!scene) return null;
|
||||
const tokenData = scene.getEmbeddedEntity("Token", tokenId);
|
||||
if (!tokenData) return null;
|
||||
const token = new Token(tokenData);
|
||||
return token.actor;
|
||||
}
|
||||
|
||||
// Case 2 - use Actor ID directory
|
||||
const actorId = card.dataset.actorId;
|
||||
return game.actors.get(actorId) || null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the Actor which is the author of a chat card
|
||||
* @param {HTMLElement} card The chat card being used
|
||||
* @return {Array.<Actor>} An Array of Actor entities, if any
|
||||
* @private
|
||||
*/
|
||||
static _getChatCardTargets(card) {
|
||||
const character = game.user.character;
|
||||
const controlled = canvas.tokens.controlled;
|
||||
const targets = controlled.reduce((arr, t) => t.actor ? arr.concat([t.actor]) : arr, []);
|
||||
if ( character && (controlled.length === 0) ) targets.push(character);
|
||||
return targets;
|
||||
}
|
||||
}
|
|
@ -1,19 +1,26 @@
|
|||
import { TraitSelector } from "../apps/trait-selector.js";
|
||||
|
||||
import TraitSelector from "../apps/trait-selector.js";
|
||||
|
||||
/**
|
||||
* Override and extend the core ItemSheet implementation to handle D&D5E specific item types
|
||||
* @type {ItemSheet}
|
||||
* Override and extend the core ItemSheet implementation to handle specific item types
|
||||
* @extends {ItemSheet}
|
||||
*/
|
||||
export class ItemSheet5e extends ItemSheet {
|
||||
export default class ItemSheet5e extends ItemSheet {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
if ( this.object.data.type === "class" ) {
|
||||
this.options.width = 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: 420,
|
||||
height: "auto",
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
resizable: false,
|
||||
resizable: true,
|
||||
scrollY: [".tab.details"],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
});
|
||||
|
@ -33,8 +40,6 @@ export class ItemSheet5e extends ItemSheet {
|
|||
getData() {
|
||||
const data = super.getData();
|
||||
data.labels = this.item.labels;
|
||||
|
||||
// Include CONFIG values
|
||||
data.config = CONFIG.SW5E;
|
||||
|
||||
// Item Type, Status, and Details
|
||||
|
@ -42,15 +47,81 @@ export class ItemSheet5e extends ItemSheet {
|
|||
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.isWeapon = data.item.type === "weapon";
|
||||
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);
|
||||
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) => {
|
||||
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;
|
||||
}
|
||||
return obj;
|
||||
}, {})
|
||||
}
|
||||
else return {};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
@ -63,10 +134,10 @@ export class ItemSheet5e extends ItemSheet {
|
|||
return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
|
||||
}
|
||||
else if ( ["weapon", "equipment"].includes(item.type) ) {
|
||||
return item.data.equipped ? "Equipped" : "Unequipped";
|
||||
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
else if ( item.type === "tool" ) {
|
||||
return item.data.proficient ? "Proficient" : "Not Proficient";
|
||||
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,8 +162,8 @@ export class ItemSheet5e extends ItemSheet {
|
|||
props.push(
|
||||
labels.components,
|
||||
labels.materials,
|
||||
item.data.components.concentration ? "Concentration" : null,
|
||||
item.data.components.ritual ? "Ritual" : null
|
||||
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
|
||||
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -108,9 +179,6 @@ export class ItemSheet5e extends ItemSheet {
|
|||
else if ( item.type === "species" ) {
|
||||
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
|
||||
=======
|
||||
else if ( item.type === "archetype" ) {
|
||||
|
||||
}
|
||||
|
@ -123,7 +191,6 @@ export class ItemSheet5e extends ItemSheet {
|
|||
|
||||
}
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
// Action type
|
||||
if ( item.data.actionType ) {
|
||||
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
||||
|
@ -143,9 +210,27 @@ export class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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={}) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
if ( !this._minimized ) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
}
|
||||
return super.setPosition(position);
|
||||
}
|
||||
|
||||
|
@ -156,33 +241,12 @@ export class ItemSheet5e extends ItemSheet {
|
|||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
// TODO: This can be removed once 0.7.x is release channel
|
||||
if ( !formData.data ) formData = expandObject(formData);
|
||||
|
||||
// Handle Damage Array
|
||||
let damage = Object.entries(formData).filter(e => e[0].startsWith("data.damage.parts"));
|
||||
formData["data.damage.parts"] = damage.reduce((arr, entry) => {
|
||||
let [i, j] = entry[0].split(".").slice(3);
|
||||
if ( !arr[i] ) arr[i] = [];
|
||||
arr[i][j] = entry[1];
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
// Handle armorproperties Array
|
||||
let armorproperties = Object.entries(formData).filter(e => e[0].startsWith("data.armorproperties.parts"));
|
||||
formData["data.armorproperties.parts"] = armorproperties.reduce((arr, entry) => {
|
||||
let [i, j] = entry[0].split(".").slice(3);
|
||||
if ( !arr[i] ) arr[i] = [];
|
||||
arr[i][j] = entry[1];
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
// Handle weaponproperties Array
|
||||
let weaponproperties = Object.entries(formData).filter(e => e[0].startsWith("data.weaponproperties.parts"));
|
||||
formData["data.weaponproperties.parts"] = weaponproperties.reduce((arr, entry) => {
|
||||
let [i, j] = entry[0].split(".").slice(3);
|
||||
if ( !arr[i] ) arr[i] = [];
|
||||
arr[i][j] = entry[1];
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
const damage = formData.data?.damage;
|
||||
if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
|
||||
|
||||
// Update the Item
|
||||
super._updateObject(event, formData);
|
||||
|
@ -194,16 +258,7 @@ export class ItemSheet5e extends ItemSheet {
|
|||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
|
||||
// Activate any Trait Selectors
|
||||
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
|
||||
|
||||
// Armor properties
|
||||
html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
|
||||
|
||||
// Weapon properties
|
||||
html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this));
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -237,64 +292,6 @@ export class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add or remove a armorproperties part from the armorproperties formula
|
||||
* @param {Event} event The original click event
|
||||
* @return {Promise}
|
||||
* @private
|
||||
*/
|
||||
async _onarmorpropertiesControl(event) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
|
||||
// Add new armorproperties component
|
||||
if ( a.classList.contains("add-armorproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const armorproperties = this.item.data.data.armorproperties;
|
||||
return this.item.update({"data.armorproperties.parts": armorproperties.parts.concat([["", ""]])});
|
||||
}
|
||||
|
||||
// Remove a armorproperties component
|
||||
if ( a.classList.contains("delete-armorproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".armorproperties-part");
|
||||
const armorproperties = duplicate(this.item.data.data.armorproperties);
|
||||
armorproperties.parts.splice(Number(li.dataset.armorpropertiesPart), 1);
|
||||
return this.item.update({"data.armorproperties.parts": armorproperties.parts});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add or remove a weaponproperties part from the weaponproperties formula
|
||||
* @param {Event} event The original click event
|
||||
* @return {Promise}
|
||||
* @private
|
||||
*/
|
||||
async _onweaponpropertiesControl(event) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
|
||||
// Add new weaponproperties component
|
||||
if ( a.classList.contains("add-weaponproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const weaponproperties = this.item.data.data.weaponproperties;
|
||||
return this.item.update({"data.weaponproperties.parts": weaponproperties.parts.concat([["", ""]])});
|
||||
}
|
||||
|
||||
// Remove a weaponproperties component
|
||||
if ( a.classList.contains("delete-weaponproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".weaponproperties-part");
|
||||
const weaponproperties = duplicate(this.item.data.data.weaponproperties);
|
||||
weaponproperties.parts.splice(Number(li.dataset.weaponpropertiesPart), 1);
|
||||
return this.item.update({"data.weaponproperties.parts": weaponproperties.parts});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue