Update Core to 1.4.1

Update Core to 1.4.1 and internal Version to 1.4.1.R1-A8
This commit is contained in:
supervj 2021-08-06 16:38:15 -04:00
parent f16383841b
commit 5bb253d9c3
56 changed files with 5440 additions and 3827 deletions

View file

@ -27,7 +27,7 @@ export default class Actor5e extends Actor {
*/
get classes() {
if (this._classes !== undefined) return this._classes;
if (this.data.type !== "character") return (this._classes = {});
if (!["character", "npc"].includes(this.data.type)) return (this._classes = {});
return (this._classes = this.items
.filter((item) => item.type === "class")
.reduce((obj, cls) => {
@ -52,6 +52,7 @@ export default class Actor5e extends Actor {
/** @override */
prepareData() {
this._preparationWarnings = [];
super.prepareData();
// iterate over owned items and recompute attributes that depend on prepared actor data
@ -62,6 +63,7 @@ export default class Actor5e extends Actor {
/** @override */
prepareBaseData() {
this._prepareBaseArmorClass(this.data);
switch (this.data.type) {
case "character":
return this._prepareCharacterData(this.data);
@ -74,6 +76,16 @@ export default class Actor5e extends Actor {
}
}
/* --------------------------------------------- */
/** @override */
applyActiveEffects() {
// The Active Effects do not have access to their parent at preparation time so we wait until this stage to
// determine whether they are suppressed or not.
this.effects.forEach((e) => e.determineSuppression());
return super.applyActiveEffects();
}
/* -------------------------------------------- */
/** @override */
@ -146,6 +158,12 @@ export default class Actor5e extends Actor {
// Prepare power-casting data
this._computeDerivedPowercasting(this.data);
// Prepare armor class data
const ac = this._computeArmorClass(data);
this.armor = ac.equippedArmor || null;
this.shield = ac.equippedShield || null;
if (ac.warnings) this._preparationWarnings.push(...ac.warnings);
}
/* -------------------------------------------- */
@ -425,6 +443,21 @@ export default class Actor5e extends Actor {
/* -------------------------------------------- */
/**
* Initialize derived AC fields for Active Effects to target.
* @param actorData
* @private
*/
_prepareBaseArmorClass(actorData) {
const ac = actorData.data.attributes.ac;
ac.base = 10;
ac.shield = ac.bonus = ac.cover = 0;
this.armor = null;
this.shield = null;
}
/* -------------------------------------------- */
/**
* Prepare data related to the power-casting capabilities of the Actor
* @private
@ -658,6 +691,103 @@ export default class Actor5e extends Actor {
/* -------------------------------------------- */
/**
* Determine a character's AC value from their equipped armor and shield.
* @param {object} data Note that this object will be mutated.
* @return {{
* calc: string,
* value: number,
* base: number,
* shield: number,
* bonus: number,
* cover: number,
* flat: number,
* equippedArmor: Item5e,
* equippedShield: Item5e,
* warnings: string[]
* }}
* @private
*/
_computeArmorClass(data) {
// Get AC configuration and apply automatic migrations for older data structures
const ac = data.attributes.ac;
ac.warnings = [];
let cfg = CONFIG.SW5E.armorClasses[ac.calc];
if (!cfg) {
ac.calc = "flat";
if (Number.isNumeric(ac.value)) ac.flat = Number(ac.value);
cfg = CONFIG.SW5E.armorClasses.flat;
}
// Identify Equipped Items
const armorTypes = new Set(Object.keys(CONFIG.SW5E.armorTypes));
const {armors, shields} = this.itemTypes.equipment.reduce(
(obj, equip) => {
const armor = equip.data.data.armor;
if (!equip.data.data.equipped || !armorTypes.has(armor?.type)) return obj;
if (armor.type === "shield") obj.shields.push(equip);
else obj.armors.push(equip);
return obj;
},
{armors: [], shields: []}
);
// Determine base AC
switch (ac.calc) {
// Flat AC (no additional bonuses)
case "flat":
ac.value = ac.flat;
return ac;
// Natural AC (includes bonuses)
case "natural":
ac.base = ac.flat;
break;
// Equipment-based AC
case "default":
if (armors.length) {
if (armors.length > 1) ac.warnings.push("SW5E.WarnMultipleArmor");
const armorData = armors[0].data.data.armor;
const isHeavy = armorData.type === "heavy";
ac.dex = isHeavy ? 0 : Math.min(armorData.dex ?? Infinity, data.abilities.dex.mod);
ac.base = (armorData.value ?? 0) + ac.dex;
ac.equippedArmor = armors[0];
} else {
ac.dex = data.abilities.dex.mod;
ac.base = 10 + ac.dex;
}
break;
// Formula-based AC
default:
let formula = ac.calc === "custom" ? ac.formula : cfg.formula;
const rollData = this.getRollData();
try {
const replaced = Roll.replaceFormulaData(formula, rollData);
ac.base = Roll.safeEval(replaced);
} catch (err) {
ac.warnings.push("SW5E.WarnBadACFormula");
const replaced = Roll.replaceFormulaData(CONFIG.SW5E.armorClasses.default.formula, rollData);
ac.base = Roll.safeEval(replaced);
}
break;
}
// Equipped Shield
if (shields.length) {
if (shields.length > 1) ac.warnings.push("SW5E.WarnMultipleShields");
ac.shield = shields[0].data.data.armor.value ?? 0;
ac.equippedShield = shields[0];
}
// Compute total AC and return
ac.value = ac.base + ac.shield + ac.bonus + ac.cover;
return ac;
}
/* -------------------------------------------- */
/**
* Prepare data related to the power-casting capabilities of the Actor
* @private
@ -712,7 +842,12 @@ export default class Actor5e extends Actor {
if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) {
const currency = actorData.data.currency;
const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0);
weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
const currencyPerWeight = game.settings.get("sw5e", "metricWeightUnits")
? CONFIG.SW5E.encumbrance.currencyPerWeight.metric
: CONFIG.SW5E.encumbrance.currencyPerWeight.imperial;
weight += numCoins / currencyPerWeight;
}
// Determine the encumbrance size class
@ -729,9 +864,14 @@ export default class Actor5e extends Actor {
// Compute Encumbrance percentage
weight = weight.toNearest(0.1);
const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod;
const strengthMultiplier = game.settings.get("sw5e", "metricWeightUnits")
? CONFIG.SW5E.encumbrance.strMultiplier.metric
: CONFIG.SW5E.encumbrance.strMultiplier.imperial;
const max = (actorData.data.abilities.str.value * strengthMultiplier * mod).toNearest(0.1);
const pct = Math.clamped((weight * 100) / max, 0, 100);
return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3};
return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 200 / 3};
}
/* -------------------------------------------- */
@ -741,7 +881,10 @@ export default class Actor5e extends Actor {
/** @inheritdoc */
async _preCreate(data, options, user) {
await super._preCreate(data, options, user);
const sourceId = this.getFlag("core", "sourceId");
if (sourceId?.startsWith("Compendium.")) return;
// Some sensible defaults for convenience
// Token size category
const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"];
this.data.token.update({width: s, height: s});
@ -1044,6 +1187,7 @@ export default class Actor5e extends Actor {
// Evaluate a global saving throw bonus
const parts = [];
const data = {};
const speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
// Include a global actor ability save bonus
const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {};
@ -1060,7 +1204,7 @@ export default class Actor5e extends Actor {
halflingLucky: this.getFlag("sw5e", "halflingLucky"),
targetValue: 10,
messageData: {
"speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
"speaker": speaker,
"flags.sw5e.roll": {type: "death"}
}
});
@ -1192,6 +1336,7 @@ export default class Actor5e extends Actor {
* @property {number} dhd Hit dice recovered or spent during the rest.
* @property {object} updateData Updates applied to the actor.
* @property {Array.<object>} updateItems Updates applied to actor's items.
* @property {boolean} longRest Whether the rest type was a long rest.
* @property {boolean} newDay Whether a new day occurred during the rest.
*/
@ -1316,7 +1461,8 @@ export default class Actor5e extends Actor {
...hitDiceUpdates,
...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay})
],
newDay: newDay
longRest,
newDay
};
// Perform updates
@ -1326,6 +1472,9 @@ export default class Actor5e extends Actor {
// Display a Chat Message summarizing the rest effects
if (chat) await this._displayRestResultMessage(result, longRest);
// Call restCompleted hook so that modules can easily perform actions when actors finish a rest
Hooks.callAll("restCompleted", this, result);
// Return data summarizing the rest effects
return result;
}
@ -1364,13 +1513,13 @@ export default class Actor5e extends Actor {
// Determine the chat message to display
if (longRest) {
message = "SW5E.LongRestResult";
if (dhp !== 0) message += "HP";
if (healthRestored) message += "HP";
if (dfp !== 0) message += "FP";
if (dtp !== 0) message += "TP";
if (dhd !== 0) message += "HD";
if (diceRestored) message += "HD";
} else {
message = "SW5E.ShortRestResultShort";
if (dhd !== 0 && dhp !== 0) {
if (diceRestored && healthRestored) {
if (dtp !== 0) {
message = "SW5E.ShortRestResultWithTech";
} else {
@ -1581,19 +1730,19 @@ export default class Actor5e extends Actor {
/**
* Transform this Actor into another one.
*
* @param {Actor} target The target Actor.
* @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con)
* @param {boolean} [keepMental] Keep mental abilities (int, wis, cha)
* @param {boolean} [keepSaves] Keep saving throw proficiencies
* @param {boolean} [keepSkills] Keep skill proficiencies
* @param {boolean} [mergeSaves] Take the maximum of the save proficiencies
* @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies
* @param {boolean} [keepClass] Keep proficiency bonus
* @param {boolean} [keepFeats] Keep features
* @param {boolean} [keepPowers] Keep powers
* @param {boolean} [keepItems] Keep items
* @param {boolean} [keepBio] Keep biography
* @param {boolean} [keepVision] Keep vision
* @param {Actor5e} target The target Actor.
* @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con)
* @param {boolean} [keepMental] Keep mental abilities (int, wis, cha)
* @param {boolean} [keepSaves] Keep saving throw proficiencies
* @param {boolean} [keepSkills] Keep skill proficiencies
* @param {boolean} [mergeSaves] Take the maximum of the save proficiencies
* @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies
* @param {boolean} [keepClass] Keep proficiency bonus
* @param {boolean} [keepFeats] Keep features
* @param {boolean} [keepPowers] Keep powers
* @param {boolean} [keepItems] Keep items
* @param {boolean} [keepBio] Keep biography
* @param {boolean} [keepVision] Keep vision
* @param {boolean} [transformTokens] Transform linked tokens too
*/
async transformInto(
@ -1649,16 +1798,16 @@ export default class Actor5e extends Actor {
d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level
d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration
d.data.powers = o.data.powers; // Keep power slots
d.data.attributes.ac.flat = target.data.data.attributes.ac.value; // Override AC
// Token appearance updates
d.token = {name: d.name};
for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) {
d.token[k] = source.token[k];
}
if (!keepVision) {
for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) {
d.token[k] = source.token[k];
}
const vision = keepVision ? o.token : source.token;
for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) {
d.token[k] = vision[k];
}
if (source.token.randomImg) {
const images = await target.getTokenImages();
@ -1745,9 +1894,9 @@ export default class Actor5e extends Actor {
const tokens = this.getActiveTokens(true);
const updates = tokens.map((t) => {
const newTokenData = foundry.utils.deepClone(d.token);
if (!t.data.actorLink) newTokenData.actorData = newActor.data;
newTokenData._id = t.data._id;
newTokenData.actorId = newActor.id;
newTokenData.actorLink = true;
return newTokenData;
});
return canvas.scene?.updateEmbeddedDocuments("Token", updates);
@ -1771,10 +1920,25 @@ export default class Actor5e extends Actor {
const baseActor = game.actors.get(this.token.data.actorId);
const prototypeTokenData = await baseActor.getTokenData();
const tokenUpdate = {actorData: {}};
for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) {
for (let k of [
"width",
"height",
"scale",
"img",
"mirrorX",
"mirrorY",
"tint",
"alpha",
"lockRotation",
"name"
]) {
tokenUpdate[k] = prototypeTokenData[k];
}
return this.token.update(tokenUpdate, {recursive: false});
await this.token.update(tokenUpdate, {recursive: false});
await this.sheet.close();
const actor = this.token.getActor();
actor.sheet.render(true);
return actor;
}
// Obtain a reference to the original actor
@ -1854,6 +2018,45 @@ export default class Actor5e extends Actor {
return type;
}
/* -------------------------------------------- */
/**
* Populate a proficiency object with a `selected` field containing a combination of
* localizable group & individual proficiencies from `value` and the contents of `custom`.
*
* @param {object} data Object containing proficiency data
* @param {Array.<string>} data.value Array of standard proficiency keys
* @param {string} data.custom Semicolon-separated string of custom proficiencies
* @param {string} type "armor", "weapon", or "tool"
*/
static prepareProficiencies(data, type) {
const profs = CONFIG.SW5E[`${type}Proficiencies`];
const itemTypes = CONFIG.SW5E[`${type}Ids`];
let values = [];
if (data.value) {
values = data.value instanceof Array ? data.value : [data.value];
}
data.selected = {};
const pack = game.packs.get(CONFIG.SW5E.sourcePacks.ITEMS);
for (const key of values) {
if (profs[key]) {
data.selected[key] = profs[key];
} else if (itemTypes && itemTypes[key]) {
const item = pack.index.get(itemTypes[key]);
data.selected[key] = item.name;
} else if (type === "tool" && CONFIG.SW5E.vehicleTypes[key]) {
data.selected[key] = CONFIG.SW5E.vehicleTypes[key];
}
}
// Add custom entries
if (data.custom) {
data.custom.split(";").forEach((c, i) => (data.selected[`custom${i + 1}`] = c.trim()));
}
}
/* -------------------------------------------- */
/* DEPRECATED METHODS */
/* -------------------------------------------- */

View file

@ -1,12 +1,16 @@
import Actor5e from "../../entity.js";
import Item5e from "../../../item/entity.js";
import ProficiencySelector from "../../../apps/proficiency-selector.js";
import PropertyAttribution from "../../../apps/property-attribution.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorArmorConfig from "../../../apps/actor-armor.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
import ActiveEffect5e from "../../../active-effect.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
@ -85,6 +89,7 @@ export default class ActorSheet5e extends ActorSheet {
// The Actor's data
const actorData = this.actor.data.toObject(false);
const source = this.actor.data._source.data;
data.actor = actorData;
data.data = actorData.data;
@ -105,6 +110,7 @@ export default class ActorSheet5e extends ActorSheet {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
abl.baseProf = source.abilities[a].proficient;
}
// Skills
@ -117,6 +123,7 @@ export default class ActorSheet5e extends ActorSheet {
skl.label = CONFIG.SW5E.starshipSkills[s];
} else {
skl.label = CONFIG.SW5E.skills[s];
skl.baseValue = source.skills[s].value;
}
}
}
@ -134,7 +141,15 @@ export default class ActorSheet5e extends ActorSheet {
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.effects);
data.effects = ActiveEffect5e.prepareActiveEffectCategories(this.actor.effects);
// Prepare warnings
data.warnings = this.actor._preparationWarnings;
// Prepare property attributions
this.attribution = {
"attributes.ac": this._prepareArmorClassAttribution(actorData.data)
};
// Return data to the sheet
return data;
@ -204,6 +219,105 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Produce a list of armor class attribution objects.
* @param {object} data Actor data to determine the attributions from.
* @return {AttributionDescription[]} List of attribution descriptions.
*/
_prepareArmorClassAttribution(data) {
const ac = data.attributes.ac;
const cfg = CONFIG.SW5E.armorClasses[ac.calc];
const attribution = [];
// Base AC Attribution
switch (ac.calc) {
// Flat AC
case "flat":
return [
{
label: game.i18n.localize("SW5E.ArmorClassFlat"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: ac.flat
}
];
// Natural armor
case "natural":
attribution.push({
label: game.i18n.localize("SW5E.ArmorClassNatural"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: ac.flat
});
break;
// Equipment-based AC
case "default":
const hasArmor = !!this.actor.armor;
attribution.push({
label: hasArmor ? this.actor.armor.name : game.i18n.localize("SW5E.ArmorClassUnarmored"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: hasArmor ? this.actor.armor.data.data.armor.value : 10
});
if (ac.dex !== 0) {
attribution.push({
label: game.i18n.localize("SW5E.AbilityDex"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.dex
});
}
break;
// Other AC formula
default:
const formula = ac.calc === "custom" ? ac.formula : cfg.formula;
let base = ac.base;
const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
for (const [match, term] of formula.matchAll(dataRgx)) {
const value = foundry.utils.getProperty(data, term);
if (term === "attributes.ac.base" || value === 0) continue;
if (Number.isNumeric(value)) base -= Number(value);
attribution.push({
label: match,
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: foundry.utils.getProperty(data, term)
});
}
attribution.unshift({
label: game.i18n.localize("SW5E.PropertyBase"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: base
});
break;
}
// Shield
if (ac.shield !== 0)
attribution.push({
label: this.actor.shield?.name ?? game.i18n.localize("SW5E.EquipmentShield"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.shield
});
// Bonus
if (ac.bonus !== 0)
attribution.push({
label: game.i18n.localize("SW5E.Bonus"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.bonus
});
// Cover
if (ac.cover !== 0)
attribution.push({
label: game.i18n.localize("SW5E.Cover"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.cover
});
return attribution;
}
/* -------------------------------------------- */
/**
* 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
@ -215,10 +329,7 @@ export default class ActorSheet5e extends ActorSheet {
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
languages: CONFIG.SW5E.languages
};
for (let [t, choices] of Object.entries(map)) {
const trait = traits[t];
@ -238,6 +349,14 @@ export default class ActorSheet5e extends ActorSheet {
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
// Populate and localize proficiencies
for (const t of ["armor", "weapon", "tool"]) {
const trait = traits[`${t}Prof`];
if (!trait) continue;
Actor5e.prepareProficiencies(trait, t);
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
@ -414,6 +533,8 @@ export default class ActorSheet5e extends ActorSheet {
// View Item Sheets
html.find(".item-edit").click(this._onItemEdit.bind(this));
// Property attributions
html.find(".attributable").mouseover(this._onPropertyAttribution.bind(this));
// Editable Only Listeners
if (this.isEditable) {
@ -429,6 +550,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find(".proficiency-selector").click(this._onProficiencySelector.bind(this));
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags
@ -446,7 +568,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
// Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
html.find(".effect-control").click((ev) => ActiveEffect5e.onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
@ -474,7 +596,7 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* Initialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
@ -517,6 +639,9 @@ export default class ActorSheet5e extends ActorSheet {
const button = event.currentTarget;
let app;
switch (button.dataset.action) {
case "armor":
app = new ActorArmorConfig(this.object);
break;
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
@ -530,7 +655,7 @@ export default class ActorSheet5e extends ActorSheet {
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
app = new ActorTypeConfig(this.object);
break;
}
app?.render(true);
@ -545,22 +670,19 @@ export default class ActorSheet5e extends ActorSheet {
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
const field = event.currentTarget.previousElementSibling;
const skillName = field.parentElement.dataset.skill;
const source = this.actor.data._source.data.skills[skillName];
if (!source) return;
// Get the current level and the array of levels
const level = parseFloat(field.val());
// Cycle to the next or previous skill level
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]);
}
let idx = levels.indexOf(source.value);
const next = idx + (event.type === "click" ? 1 : 3);
field.value = levels[next % 4];
// Update the field value and save the form
this._onSubmit(event);
return this._onSubmit(event);
}
/* -------------------------------------------- */
@ -674,7 +796,12 @@ export default class ActorSheet5e extends ActorSheet {
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
return (
sourceId &&
sourceId === itemData.flags.core?.sourceId &&
i.type === "consumable" &&
i.name === itemData.name
);
});
if (similarItem && itemData.name !== "Power Cell") {
// Always create a new powercell instead of increasing quantity
@ -901,6 +1028,22 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle displaying the property attribution tooltip when a property is hovered over.
* @param {Event} event The originating mouse event.
* @private
*/
async _onPropertyAttribution(event) {
const existingTooltip = event.currentTarget.querySelector("div.tooltip");
const property = event.currentTarget.dataset.property;
if (existingTooltip || !property || !this.attribution) return;
let html = await new PropertyAttribution(this.object, this.attribution, property).renderTooltip();
event.currentTarget.insertAdjacentElement("beforeend", html[0]);
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
@ -957,6 +1100,22 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle spawning the ProficiencySelector application to configure armor, weapon, and tool proficiencies.
* @param {Event} event The click event which originated the selection
* @private
*/
_onProficiencySelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const options = {name: a.dataset.target, title: label.innerText, type: a.dataset.type};
if (options.type === "tool") options.sortCategories = true;
return new ProficiencySelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection

View file

@ -62,6 +62,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
})
.join(", ");
// Weight unit
sheetData["weightUnit"] = game.settings.get("sw5e", "metricWeightUnits")
? game.i18n.localize("SW5E.AbbreviationKgs")
: game.i18n.localize("SW5E.AbbreviationLbs");
// Return data for rendering
return sheetData;

View file

@ -111,9 +111,28 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
// Armor Type
data.labels["armorType"] = this.getArmorLabel();
return data;
}
/* -------------------------------------------- */
/**
* Format NPC armor information into a localized string.
* @return {string} Formatted armor label.
*/
getArmorLabel() {
const ac = this.actor.data.data.attributes.ac;
const label = [];
if (ac.calc === "default") label.push(this.actor.armor?.name || game.i18n.localize("SW5E.ArmorClassUnarmored"));
else label.push(game.i18n.localize(CONFIG.SW5E.armorClasses[ac.calc].label));
if (this.actor.shield) label.push(this.actor.shield.name);
return label.filterJoin(", ");
}
/* -------------------------------------------- */
/* Object Updates */
/* -------------------------------------------- */

View file

@ -13,7 +13,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
width: 720,
height: 680
});
}
@ -47,10 +47,17 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
const currencyPerWeight = game.settings.get("sw5e", "metricWeightUnits")
? CONFIG.SW5E.encumbrance.currencyPerWeight.metric
: CONFIG.SW5E.encumbrance.currencyPerWeight.imperial;
totalWeight += totalCoins / currencyPerWeight;
// Vehicle weights are an order of magnitude greater.
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
totalWeight /= game.settings.get("sw5e", "metricWeightUnits")
? CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.metric
: CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.imperial;
// Compute overall encumbrance
const max = actorData.data.attributes.capacity.cargo;
@ -80,12 +87,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
// Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
@ -114,7 +119,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity"
property: "data.quantity",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.AC"),
@ -138,14 +144,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
crewable: true,
dataset: {"type": "feat", "activation.type": "crew"},
columns: [
{
label: game.i18n.localize("SW5E.VehicleCrew"),
css: "item-crew",
property: "crew"
},
{
label: game.i18n.localize("SW5E.Cover"),
css: "item-cover",
@ -179,6 +181,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
}
};
data.items.forEach((item) => {
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;
});
const cargo = {
crew: {
label: game.i18n.localize("SW5E.VehicleCrew"),
@ -284,6 +293,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
.click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this));
html.find(".item-qty:not(.cargo) input")
.click((evt) => evt.target.select())
.change(this._onQtyChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide();
}
@ -416,6 +429,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Special handling for editing quantity value of equipment and weapons inside the features tab.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onQtyChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const qty = parseInt(event.currentTarget.value);
event.currentTarget.value = qty;
return item.update({"data.quantity": qty});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}

View file

@ -1,4 +1,7 @@
import Actor5e from "../../entity.js";
import Item5e from "../../../item/entity.js";
import ProficiencySelector from "../../../apps/proficiency-selector.js";
import PropertyAttribution from "../../../apps/property-attribution.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
@ -6,7 +9,7 @@ import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
import ActiveEffect5e from "../../../active-effect.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
@ -82,6 +85,7 @@ export default class ActorSheet5e extends ActorSheet {
// The Actor's data
const actorData = this.actor.data.toObject(false);
const source = this.actor.data._source.data;
data.actor = actorData;
data.data = actorData.data;
@ -102,6 +106,7 @@ export default class ActorSheet5e extends ActorSheet {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
abl.baseProf = source.abilities[a].proficient;
}
// Skills
@ -111,6 +116,7 @@ export default class ActorSheet5e extends ActorSheet {
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
skl.baseValue = source.skills[s].value;
}
}
@ -127,7 +133,15 @@ export default class ActorSheet5e extends ActorSheet {
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.effects);
data.effects = ActiveEffect5e.prepareActiveEffectCategories(this.actor.effects);
// Prepare warnings
data.warnings = this.actor._preparationWarnings;
// Prepare property attributions
this.attribution = {
"attributes.ac": this._prepareArmorClassAttribution(actorData.data)
};
// Return data to the sheet
return data;
@ -197,6 +211,105 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Produce a list of armor class attribution objects.
* @param {object} data Actor data to determine the attributions from.
* @return {AttributionDescription[]} List of attribution descriptions.
*/
_prepareArmorClassAttribution(data) {
const ac = data.attributes.ac;
const cfg = CONFIG.SW5E.armorClasses[ac.calc];
const attribution = [];
// Base AC Attribution
switch (ac.calc) {
// Flat AC
case "flat":
return [
{
label: game.i18n.localize("SW5E.ArmorClassFlat"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: ac.flat
}
];
// Natural armor
case "natural":
attribution.push({
label: game.i18n.localize("SW5E.ArmorClassNatural"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: ac.flat
});
break;
// Equipment-based AC
case "default":
const hasArmor = !!this.actor.armor;
attribution.push({
label: hasArmor ? this.actor.armor.name : game.i18n.localize("SW5E.ArmorClassUnarmored"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: hasArmor ? this.actor.armor.data.data.armor.value : 10
});
if (ac.dex !== 0) {
attribution.push({
label: game.i18n.localize("SW5E.AbilityDex"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.dex
});
}
break;
// Other AC formula
default:
const formula = ac.calc === "custom" ? ac.formula : cfg.formula;
let base = ac.base;
const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
for (const [match, term] of formula.matchAll(dataRgx)) {
const value = foundry.utils.getProperty(data, term);
if (term === "attributes.ac.base" || value === 0) continue;
if (Number.isNumeric(value)) base -= Number(value);
attribution.push({
label: match,
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: foundry.utils.getProperty(data, term)
});
}
attribution.unshift({
label: game.i18n.localize("SW5E.PropertyBase"),
mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,
value: base
});
break;
}
// Shield
if (ac.shield !== 0)
attribution.push({
label: this.actor.shield?.name ?? game.i18n.localize("SW5E.EquipmentShield"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.shield
});
// Bonus
if (ac.bonus !== 0)
attribution.push({
label: game.i18n.localize("SW5E.Bonus"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.bonus
});
// Cover
if (ac.cover !== 0)
attribution.push({
label: game.i18n.localize("SW5E.Cover"),
mode: CONST.ACTIVE_EFFECT_MODES.ADD,
value: ac.cover
});
return attribution;
}
/* -------------------------------------------- */
/**
* 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
@ -208,10 +321,7 @@ export default class ActorSheet5e extends ActorSheet {
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
languages: CONFIG.SW5E.languages
};
for (let [t, choices] of Object.entries(map)) {
const trait = traits[t];
@ -231,6 +341,14 @@ export default class ActorSheet5e extends ActorSheet {
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
// Populate and localize proficiencies
for (const t of ["armor", "weapon", "tool"]) {
const trait = traits[`${t}Prof`];
if (!trait) continue;
Actor5e.prepareProficiencies(trait, t);
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
@ -418,6 +536,9 @@ export default class ActorSheet5e extends ActorSheet {
// View Item Sheets
html.find(".item-edit").click(this._onItemEdit.bind(this));
// Property attributions
html.find(".attributable").mouseover(this._onPropertyAttribution.bind(this));
// Editable Only Listeners
if (this.isEditable) {
// Input focus and update
@ -432,6 +553,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find(".proficiency-selector").click(this._onProficiencySelector.bind(this));
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags
@ -446,7 +568,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
html.find(".effect-control").click((ev) => ActiveEffect5e.onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
@ -474,7 +596,7 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* Initialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
@ -517,6 +639,9 @@ export default class ActorSheet5e extends ActorSheet {
const button = event.currentTarget;
let app;
switch (button.dataset.action) {
case "armor":
app = new ActorArmorConfig(this.object);
break;
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
@ -530,7 +655,7 @@ export default class ActorSheet5e extends ActorSheet {
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
app = new ActorTypeConfig(this.object);
break;
}
app?.render(true);
@ -545,22 +670,19 @@ export default class ActorSheet5e extends ActorSheet {
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
const field = event.currentTarget.previousElementSibling;
const skillName = field.parentElement.dataset.skill;
const source = this.actor.data._source.data.skills[skillName];
if (!source) return;
// Get the current level and the array of levels
const level = parseFloat(field.val());
// Cycle to the next or previous skill level
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]);
}
let idx = levels.indexOf(source.value);
const next = idx + (event.type === "click" ? 1 : 3);
field.value = levels[next % 4];
// Update the field value and save the form
this._onSubmit(event);
return this._onSubmit(event);
}
/* -------------------------------------------- */
@ -675,7 +797,12 @@ export default class ActorSheet5e extends ActorSheet {
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
return (
sourceId &&
sourceId === itemData.flags.core?.sourceId &&
i.type === "consumable" &&
i.name === itemData.name
);
});
if (similarItem) {
return similarItem.update({
@ -832,6 +959,22 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle displaying the property attribution tooltip when a property is hovered over.
* @param {Event} event The originating mouse event.
* @private
*/
async _onPropertyAttribution(event) {
const existingTooltip = event.currentTarget.querySelector("div.tooltip");
const property = event.currentTarget.dataset.property;
if (existingTooltip || !property || !this.attribution) return;
let html = await new PropertyAttribution(this.object, this.attribution, property).renderTooltip();
event.currentTarget.insertAdjacentElement("beforeend", html[0]);
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
@ -888,6 +1031,22 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle spawning the ProficiencySelector application to configure armor, weapon, and tool proficiencies.
* @param {Event} event The click event which originated the selection
* @private
*/
_onProficiencySelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const options = {name: a.dataset.target, title: label.innerText, type: a.dataset.type};
if (options.type === "tool") options.sortCategories = true;
return new ProficiencySelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection

View file

@ -51,6 +51,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
})
.join(", ");
// Weight unit
sheetData["weightUnit"] = game.settings.get("sw5e", "metricWeightUnits")
? game.i18n.localize("SW5E.AbbreviationKgs")
: game.i18n.localize("SW5E.AbbreviationLbs");
// Return data for rendering
return sheetData;
}

View file

@ -96,9 +96,28 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
// Armor Type
data.labels["armorType"] = this.getArmorLabel();
return data;
}
/* -------------------------------------------- */
/**
* Format NPC armor information into a localized string.
* @return {string} Formatted armor label.
*/
getArmorLabel() {
const ac = this.actor.data.data.attributes.ac;
const label = [];
if (ac.calc === "default") label.push(this.actor.armor?.name || game.i18n.localize("SW5E.ArmorClassUnarmored"));
else label.push(game.i18n.localize(CONFIG.SW5E.armorClasses[ac.calc].label));
if (this.actor.shield) label.push(this.actor.shield.name);
return label.filterJoin(", ");
}
/* -------------------------------------------- */
/* Object Updates */
/* -------------------------------------------- */

View file

@ -13,7 +13,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
width: 720,
height: 680
});
}
@ -47,10 +47,17 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
const currencyPerWeight = game.settings.get("sw5e", "metricWeightUnits")
? CONFIG.SW5E.encumbrance.currencyPerWeight.metric
: CONFIG.SW5E.encumbrance.currencyPerWeight.imperial;
totalWeight += totalCoins / currencyPerWeight;
// Vehicle weights are an order of magnitude greater.
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
totalWeight /= game.settings.get("sw5e", "metricWeightUnits")
? CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.metric
: CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.imperial;
// Compute overall encumbrance
const max = actorData.data.attributes.capacity.cargo;
@ -80,12 +87,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
// Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
@ -114,7 +119,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity"
property: "data.quantity",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.AC"),
@ -138,14 +144,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
crewable: true,
dataset: {"type": "feat", "activation.type": "crew"},
columns: [
{
label: game.i18n.localize("SW5E.VehicleCrew"),
css: "item-crew",
property: "crew"
},
{
label: game.i18n.localize("SW5E.Cover"),
css: "item-cover",
@ -179,6 +181,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
}
};
data.items.forEach((item) => {
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;
});
const cargo = {
crew: {
label: game.i18n.localize("SW5E.VehicleCrew"),
@ -284,6 +293,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
.click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this));
html.find(".item-qty:not(.cargo) input")
.click((evt) => evt.target.select())
.change(this._onQtyChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide();
}
@ -416,6 +429,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Special handling for editing quantity value of equipment and weapons inside the features tab.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onQtyChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const qty = parseInt(event.currentTarget.value);
event.currentTarget.value = qty;
return item.update({"data.quantity": qty});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}