forked from GitHub-Mirrors/foundry-sw5e
DND5e Core 1.2.0
DND5e Core update 1.2.0 modded to SW5e
This commit is contained in:
parent
a544f5e0a9
commit
ab510e336c
59 changed files with 2505 additions and 2231 deletions
|
@ -1,8 +1,6 @@
|
|||
import { d20Roll, damageRoll } from "../dice.js";
|
||||
import ShortRestDialog from "../apps/short-rest.js";
|
||||
import LongRestDialog from "../apps/long-rest.js";
|
||||
import AbilityUseDialog from "../apps/ability-use-dialog.js";
|
||||
import AbilityTemplate from "../pixi/ability-template.js";
|
||||
import {SW5E} from '../config.js';
|
||||
|
||||
/**
|
||||
|
@ -208,7 +206,7 @@ export default class Actor5e extends Actor {
|
|||
const updateData = expandObject(u);
|
||||
const config = {
|
||||
className: updateData.name || item.data.name,
|
||||
archetypeName: updateData.data.archetype || item.data.data.archetype,
|
||||
archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype,
|
||||
level: getProperty(updateData, "data.levels"),
|
||||
priorLevel: item ? item.data.data.levels : 0
|
||||
}
|
||||
|
@ -357,16 +355,8 @@ export default class Actor5e extends Actor {
|
|||
const data = actorData.data;
|
||||
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
|
||||
|
||||
// Apply powercasting DC to any power items which use it
|
||||
for ( let i of this.items ) {
|
||||
const save = i.data.data.save;
|
||||
if ( save?.ability ) {
|
||||
if ( save.scaling === "power" ) save.dc = data.attributes.powerdc;
|
||||
else if ( save.scaling !== "flat" ) save.dc = data.abilities[save.scaling]?.dc ?? 10;
|
||||
const ability = CONFIG.SW5E.abilities[save.ability];
|
||||
i.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability});
|
||||
}
|
||||
}
|
||||
// Compute ability save DCs that depend on the calling actor
|
||||
this.items.forEach(i => i.getSaveDC());
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -547,20 +537,67 @@ export default class Actor5e extends Actor {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async createOwnedItem(itemData, options) {
|
||||
async createEmbeddedEntity(embeddedName, itemData, options={}) {
|
||||
|
||||
// Assume NPCs are always proficient with weapons and always have powers prepared
|
||||
if ( !this.isPC ) {
|
||||
let t = itemData.type;
|
||||
let initial = {};
|
||||
if ( t === "weapon" ) initial["data.proficient"] = true;
|
||||
if ( ["weapon", "equipment"].includes(t) ) initial["data.equipped"] = true;
|
||||
if ( t === "power" ) initial["data.prepared"] = true;
|
||||
mergeObject(itemData, initial);
|
||||
}
|
||||
return super.createOwnedItem(itemData, options);
|
||||
// Pre-creation steps for owned items
|
||||
if ( embeddedName === "OwnedItem" ) this._preCreateOwnedItem(itemData, options);
|
||||
|
||||
// Standard embedded entity creation
|
||||
return super.createEmbeddedEntity(embeddedName, itemData, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer
|
||||
* @param itemData
|
||||
* @param options
|
||||
* @private
|
||||
*/
|
||||
_preCreateOwnedItem(itemData, options) {
|
||||
if ( this.data.type === "vehicle" ) return;
|
||||
const isNPC = this.data.type === 'npc';
|
||||
let initial = {};
|
||||
switch ( itemData.type ) {
|
||||
case "weapon":
|
||||
initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
|
||||
let hasWeaponProf = isNPC; // NPCs automatically have weapon proficiency
|
||||
if ( !isNPC ) {
|
||||
const weaponProf = {
|
||||
"natural": true,
|
||||
"simpleM": "sim",
|
||||
"simpleR": "sim",
|
||||
"martialM": "mar",
|
||||
"martialR": "mar"
|
||||
}[itemData.data?.weaponType];
|
||||
const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
|
||||
hasWeaponProf = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
|
||||
}
|
||||
initial["data.proficient"] = hasWeaponProf;
|
||||
break;
|
||||
case "equipment":
|
||||
initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
|
||||
let hasEquipmentProf = isNPC; // NPCs automatically have equipment proficiency
|
||||
if ( !isNPC ) {
|
||||
const armorProf = {
|
||||
"natural": true,
|
||||
"clothing": true,
|
||||
"light": "lgt",
|
||||
"medium": "med",
|
||||
"heavy": "hvy",
|
||||
"shield": "shl"
|
||||
}[itemData.data?.armor?.type];
|
||||
const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
|
||||
hasEquipmentProf = (armorProf === true) || actorArmorProfs.includes(armorProf);
|
||||
}
|
||||
initial["data.proficient"] = hasEquipmentProf;
|
||||
break;
|
||||
case "power":
|
||||
initial["data.prepared"] = true; // NPCs automatically prepare powers
|
||||
break;
|
||||
}
|
||||
mergeObject(itemData, initial);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Gameplay Mechanics */
|
||||
|
@ -601,77 +638,16 @@ export default class Actor5e extends Actor {
|
|||
"data.attributes.hp.temp": tmp - dt,
|
||||
"data.attributes.hp.value": dh
|
||||
};
|
||||
return this.update(updates);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cast a Power, consuming a power slot of a certain level
|
||||
* @param {Item5e} item The power being cast by the actor
|
||||
* @param {Event} event The originating user interaction which triggered the cast
|
||||
*/
|
||||
async usePower(item, {configureDialog=true}={}) {
|
||||
if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
|
||||
const itemData = item.data.data;
|
||||
|
||||
// Configure powercasting data
|
||||
let lvl = itemData.level;
|
||||
const usesSlots = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
const limitedUses = !!itemData.uses.per;
|
||||
let consumeSlot = `power${lvl}`;
|
||||
let consumeUse = false;
|
||||
let placeTemplate = false;
|
||||
|
||||
// Configure power slot consumption and measured template placement from the form
|
||||
if ( configureDialog && (usesSlots || item.hasAreaTarget || limitedUses) ) {
|
||||
const usage = await AbilityUseDialog.create(item);
|
||||
if ( usage === null ) return;
|
||||
|
||||
// Determine consumption preferences
|
||||
consumeSlot = Boolean(usage.get("consumeSlot"));
|
||||
consumeUse = Boolean(usage.get("consumeUse"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
|
||||
// Determine the cast power level
|
||||
const isPact = usage.get('level') === 'pact';
|
||||
const lvl = isPact ? this.data.data.powers.pact.level : parseInt(usage.get("level"));
|
||||
if ( lvl !== item.data.data.level ) {
|
||||
const upcastData = mergeObject(item.data, {"data.level": lvl}, {inplace: false});
|
||||
item = item.constructor.createOwned(upcastData, this);
|
||||
}
|
||||
|
||||
// Denote the power slot being consumed
|
||||
if ( consumeSlot ) consumeSlot = isPact ? "pact" : `power${lvl}`;
|
||||
}
|
||||
|
||||
// Update Actor data
|
||||
if ( usesSlots && consumeSlot && (lvl > 0) ) {
|
||||
const slots = parseInt(this.data.data.powers[consumeSlot]?.value);
|
||||
if ( slots === 0 || Number.isNaN(slots) ) {
|
||||
return ui.notifications.error(game.i18n.localize("SW5E.PowerCastNoSlots"));
|
||||
}
|
||||
await this.update({
|
||||
[`data.powers.${consumeSlot}.value`]: Math.max(slots - 1, 0)
|
||||
});
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
if ( limitedUses && consumeUse ) {
|
||||
const uses = parseInt(itemData.uses.value || 0);
|
||||
if ( uses <= 0 ) ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: item.name}));
|
||||
await item.update({"data.uses.value": Math.max(parseInt(item.data.data.uses.value || 0) - 1, 0)})
|
||||
}
|
||||
|
||||
// Initiate ability template placement workflow if selected
|
||||
if ( placeTemplate && item.hasAreaTarget ) {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.sheet.rendered ) this.sheet.minimize();
|
||||
}
|
||||
|
||||
// Invoke the Item roll
|
||||
return item.roll();
|
||||
// Delegate damage application to a hook
|
||||
// TODO replace this in the future with a better modifyTokenAttribute function in the core
|
||||
const allowed = Hooks.call("modifyTokenAttribute", {
|
||||
attribute: "attributes.hp",
|
||||
value: amount,
|
||||
isDelta: false,
|
||||
isBar: true
|
||||
}, updates);
|
||||
return allowed !== false ? this.update(updates) : this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -996,7 +972,7 @@ export default class Actor5e extends Actor {
|
|||
// Adjust actor data
|
||||
await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
|
||||
const hp = this.data.data.attributes.hp;
|
||||
const dhp = Math.min(hp.max - hp.value, roll.total);
|
||||
const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
|
||||
await this.update({"data.attributes.hp.value": hp.value + dhp});
|
||||
return roll;
|
||||
}
|
||||
|
@ -1461,4 +1437,18 @@ export default class Actor5e extends Actor {
|
|||
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`);
|
||||
return this.data.data.abilities[ability]?.dc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cast a Power, consuming a power slot of a certain level
|
||||
* @param {Item5e} item The power being cast by the actor
|
||||
* @param {Event} event The originating user interaction which triggered the cast
|
||||
* @deprecated since sw5e 1.2.0
|
||||
*/
|
||||
async usePower(item, {configureDialog=true}={}) {
|
||||
console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
|
||||
if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
|
||||
return item.roll();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import Item5e from "../../item/entity.js";
|
||||
import TraitSelector from "../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../apps/actor-flags.js";
|
||||
import MovementConfig from "../../apps/movement-config.js";
|
||||
import ActorMovementConfig from "../../apps/movement-config.js";
|
||||
import ActorSensesConfig from "../../apps/senses-config.js";
|
||||
import {SW5E} from '../../config.js';
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
|
||||
|
||||
|
@ -99,6 +100,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
// Movement speeds
|
||||
data.movement = this._getMovementSpeed(data.actor);
|
||||
|
||||
// Senses
|
||||
data.senses = this._getSenses(data.actor);
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
|
||||
|
@ -121,7 +125,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData) {
|
||||
const movement = actorData.data.attributes.movement;
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
const speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
|
@ -136,6 +140,20 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_getSenses(actorData) {
|
||||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
||||
const v = senses[k] ?? 0
|
||||
if ( v === 0 ) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
if ( !!senses.special ) tags["special"] = senses.special;
|
||||
return tags;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
|
||||
* @param {object} traits The raw traits data object from the actor data
|
||||
|
@ -373,8 +391,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
|
||||
|
||||
// Configure Special Flags
|
||||
html.find('.configure-movement').click(this._onMovementConfig.bind(this));
|
||||
html.find('.configure-flags').click(this._onConfigureFlags.bind(this));
|
||||
html.find('.config-button').click(this._onConfigMenu.bind(this));
|
||||
|
||||
// Owned Item management
|
||||
html.find('.item-create').click(this._onItemCreate.bind(this));
|
||||
|
@ -448,11 +465,24 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle click events for the Traits tab button to configure special Character Flags
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onConfigureFlags(event) {
|
||||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
new ActorSheetFlags(this.actor).render(true);
|
||||
const button = event.currentTarget;
|
||||
switch ( button.dataset.action ) {
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -529,6 +559,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
mergeSaves: true,
|
||||
mergeSkills: true,
|
||||
|
@ -619,14 +651,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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();
|
||||
return item.roll();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -687,7 +712,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
data: duplicate(header.dataset)
|
||||
};
|
||||
delete itemData.data["type"];
|
||||
return this.actor.createOwnedItem(itemData);
|
||||
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -791,18 +816,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onMovementConfig(event) {
|
||||
event.preventDefault();
|
||||
new MovementConfig(this.object).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getHeaderButtons() {
|
||||
let buttons = super._getHeaderButtons();
|
||||
|
|
|
@ -75,6 +75,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.attunement = {
|
||||
1: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
2: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
|
@ -248,37 +260,21 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
let addLevel = false;
|
||||
|
||||
// Upgrade the number of class levels a character has and add features
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
const hasClass = !!cls;
|
||||
|
||||
// Increment levels instead of creating a new item
|
||||
if ( hasClass ) {
|
||||
if ( !!cls ) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if ( next > priorLevel ) {
|
||||
itemData.levels = next;
|
||||
await cls.update({"data.levels": next});
|
||||
addLevel = true;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
}
|
||||
|
||||
// Add class features
|
||||
if ( !hasClass || addLevel ) {
|
||||
const features = await Actor5e.getClassFeatures({
|
||||
className: itemData.name,
|
||||
archetypeName: itemData.data.archetype,
|
||||
level: itemData.levels,
|
||||
priorLevel: priorLevel
|
||||
});
|
||||
await this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
}
|
||||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
if ( !addLevel ) super._onDropItemCreate(itemData);
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,15 +34,19 @@ export default class AbilityUseDialog extends Dialog {
|
|||
const quantity = itemData.quantity || 0;
|
||||
const recharge = itemData.recharge || {};
|
||||
const recharges = !!recharge.value;
|
||||
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
|
||||
|
||||
// Prepare dialog form data
|
||||
const data = {
|
||||
item: item.data,
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
|
||||
note: this._getAbilityUseNote(item.data, uses, recharge),
|
||||
hasLimitedUses: uses.max || recharges,
|
||||
canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
consumePowerSlot: false,
|
||||
consumeRecharge: recharges,
|
||||
consumeResource: !!itemData.consume.target,
|
||||
consumeUses: uses.max,
|
||||
canUse: recharges ? recharge.charged : sufficientUses,
|
||||
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
errors: []
|
||||
};
|
||||
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
|
||||
|
@ -50,7 +54,7 @@ export default class AbilityUseDialog extends Dialog {
|
|||
// Render the ability usage template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
// Create the Dialog and return data as a Promise
|
||||
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
|
||||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
||||
return new Promise((resolve) => {
|
||||
|
@ -61,7 +65,10 @@ export default class AbilityUseDialog extends Dialog {
|
|||
use: {
|
||||
icon: `<i class="fas ${icon}"></i>`,
|
||||
label: label,
|
||||
callback: html => resolve(new FormData(html[0].querySelector("form")))
|
||||
callback: html => {
|
||||
const fd = new FormDataExtended(html[0].querySelector("form"));
|
||||
resolve(fd.toObject());
|
||||
}
|
||||
}
|
||||
},
|
||||
default: "use",
|
||||
|
@ -83,11 +90,11 @@ export default class AbilityUseDialog extends Dialog {
|
|||
|
||||
// Determine whether the power may be up-cast
|
||||
const lvl = itemData.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
|
||||
// If can't upcast, return early and don't bother calculating available power slots
|
||||
if (!canUpcast) {
|
||||
data = mergeObject(data, { isPower: true, canUpcast });
|
||||
if (!consumePowerSlot) {
|
||||
mergeObject(data, { isPower: true, consumePowerSlot });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -122,7 +129,7 @@ export default class AbilityUseDialog extends Dialog {
|
|||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
|
||||
// Return merged data
|
||||
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
|
||||
data = mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
|
||||
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
|
||||
}
|
||||
|
||||
|
|
|
@ -67,10 +67,10 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
|
||||
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
|
||||
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
|
||||
{name: "data.bonuses.msak.attack", label: "SW5E.BonusMSAttack"},
|
||||
{name: "data.bonuses.msak.damage", label: "SW5E.BonusMSDamage"},
|
||||
{name: "data.bonuses.rsak.attack", label: "SW5E.BonusRSAttack"},
|
||||
{name: "data.bonuses.rsak.damage", label: "SW5E.BonusRSDamage"},
|
||||
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
|
||||
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
|
||||
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
|
||||
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
|
||||
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
|
||||
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
|
||||
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
|
||||
|
|
|
@ -2,21 +2,27 @@
|
|||
* A simple form to set actor movement speeds
|
||||
* @implements {BaseEntitySheet}
|
||||
*/
|
||||
export default class MovementConfig extends BaseEntitySheet {
|
||||
export default class ActorMovementConfig extends BaseEntitySheet {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
title: "SW5E.MovementConfig",
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/movement-config.html",
|
||||
width: 240,
|
||||
width: 300,
|
||||
height: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const data = {
|
||||
|
|
43
module/apps/senses-config.js
Normal file
43
module/apps/senses-config.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* A simple form to set actor movement speeds
|
||||
* @implements {BaseEntitySheet}
|
||||
*/
|
||||
export default class ActorSensesConfig extends BaseEntitySheet {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/senses-config.html",
|
||||
width: 300,
|
||||
height: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const senses = this.entity._data.data.attributes?.senses ?? {};
|
||||
const data = {
|
||||
senses: {},
|
||||
special: senses.special ?? "",
|
||||
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
|
||||
};
|
||||
for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
||||
const v = senses[name];
|
||||
data.senses[name] = {
|
||||
label: game.i18n.localize(label),
|
||||
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* A specialized form used to select from a checklist of attributes, traits, or properties
|
||||
* @extends {FormApplication}
|
||||
* @implements {FormApplication}
|
||||
*/
|
||||
export default class TraitSelector extends FormApplication {
|
||||
|
||||
|
@ -36,8 +36,8 @@ export default class TraitSelector extends FormApplication {
|
|||
getData() {
|
||||
|
||||
// Get current values
|
||||
let attr = getProperty(this.object.data, this.attribute) || {};
|
||||
attr.value = attr.value || [];
|
||||
let attr = getProperty(this.object._data, this.attribute);
|
||||
if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
|
||||
|
||||
// Populate choices
|
||||
const choices = duplicate(this.options.choices);
|
||||
|
@ -49,7 +49,7 @@ export default class TraitSelector extends FormApplication {
|
|||
}
|
||||
|
||||
// Return data
|
||||
return {
|
||||
return {
|
||||
allowCustom: this.options.allowCustom,
|
||||
choices: choices,
|
||||
custom: attr ? attr.custom : ""
|
||||
|
|
|
@ -54,6 +54,20 @@ SW5E.alignments = {
|
|||
'ce': "SW5E.AlignmentCE"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An enumeration of item attunement states
|
||||
* @type {{"0": string, "1": string, "2": string}}
|
||||
*/
|
||||
SW5E.attunements = {
|
||||
0: "SW5E.AttunementNone",
|
||||
1: "SW5E.AttunementRequired",
|
||||
2: "SW5E.AttunementAttuned"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
SW5E.weaponProficiencies = {
|
||||
"sim": "SW5E.WeaponSimpleProficiency",
|
||||
|
@ -155,8 +169,8 @@ SW5E.tokenSizes = {
|
|||
SW5E.itemActionTypes = {
|
||||
"mwak": "SW5E.ActionMWAK",
|
||||
"rwak": "SW5E.ActionRWAK",
|
||||
"msak": "SW5E.ActionMSAK",
|
||||
"rsak": "SW5E.ActionRSAK",
|
||||
"mpak": "SW5E.ActionMPAK",
|
||||
"rpak": "SW5E.ActionRPAK",
|
||||
"save": "SW5E.ActionSave",
|
||||
"heal": "SW5E.ActionHeal",
|
||||
"abil": "SW5E.ActionAbil",
|
||||
|
@ -396,17 +410,16 @@ SW5E.hitDieTypes = ["d6", "d8", "d10", "d12"];
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Character senses options
|
||||
* @type {Object}
|
||||
* The set of possible sensory perception types which an Actor may have
|
||||
* @type {object}
|
||||
*/
|
||||
SW5E.senses = {
|
||||
"bs": "SW5E.SenseBS",
|
||||
"dv": "SW5E.SenseDV",
|
||||
"ts": "SW5E.SenseTS",
|
||||
"tr": "SW5E.SenseTR"
|
||||
"blindsight": "SW5E.SenseBlindsight",
|
||||
"darkvision": "SW5E.SenseDarkvision",
|
||||
"tremorsense": "SW5E.SenseTremorsense",
|
||||
"truesight": "SW5E.SenseTruesight"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
|
|
@ -214,7 +214,6 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
|
|||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, crit, form) {
|
||||
|
@ -236,6 +235,7 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
|
|||
roll.terms[0].alter(1, criticalBonusDice);
|
||||
roll._formula = roll.formula;
|
||||
}
|
||||
roll.dice.forEach(d => d.options.critical = true);
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
|
||||
}
|
||||
|
@ -251,7 +251,7 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
|
|||
};
|
||||
|
||||
// Create the Roll instance
|
||||
const roll = fastForward ? _roll(parts, critical || event.altKey) : await _damageRollDialog({
|
||||
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
|
||||
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
|
||||
});
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class Item5e extends Item {
|
|||
* @type {boolean}
|
||||
*/
|
||||
get hasAttack() {
|
||||
return ["mwak", "rwak", "msak", "rsak"].includes(this.data.data.actionType);
|
||||
return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -101,7 +101,8 @@ export default class Item5e extends Item {
|
|||
* @type {boolean}
|
||||
*/
|
||||
get hasSave() {
|
||||
return !!(this.data.data.save && this.data.data.save.ability);
|
||||
const save = this.data.data?.save || {};
|
||||
return !!(save.ability && save.scaling);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -152,7 +153,7 @@ export default class Item5e extends Item {
|
|||
const itemData = this.data;
|
||||
const data = itemData.data;
|
||||
const C = CONFIG.SW5E;
|
||||
const labels = {};
|
||||
const labels = this.labels = {};
|
||||
|
||||
// Classes
|
||||
if ( itemData.type === "class" ) {
|
||||
|
@ -223,12 +224,8 @@ export default class Item5e extends Item {
|
|||
// Item Actions
|
||||
if ( data.hasOwnProperty("actionType") ) {
|
||||
|
||||
// Saving throws for unowned items
|
||||
const save = data.save;
|
||||
if ( save?.ability && !this.isOwned ) {
|
||||
if ( save.scaling !== "flat" ) save.dc = null;
|
||||
labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: C.abilities[save.ability]});
|
||||
}
|
||||
// Saving throws
|
||||
this.getSaveDC();
|
||||
|
||||
// Damage
|
||||
let dam = data.damage || {};
|
||||
|
@ -236,10 +233,43 @@ export default class Item5e extends Item {
|
|||
labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
|
||||
labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", ");
|
||||
}
|
||||
|
||||
// Limited Uses
|
||||
if ( this.isOwned && !!data.uses?.max ) {
|
||||
let max = data.uses.max;
|
||||
if ( !Number.isNumeric(max) ) {
|
||||
max = Roll.replaceFormulaData(max, this.actor.getRollData());
|
||||
if ( Roll.MATH_PROXY.safeEval ) max = Roll.MATH_PROXY.safeEval(max);
|
||||
}
|
||||
data.uses.max = Number(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the derived power DC for an item that requires a saving throw
|
||||
* @returns {number|null}
|
||||
*/
|
||||
getSaveDC() {
|
||||
if ( !this.hasSave ) return;
|
||||
const save = this.data.data?.save;
|
||||
|
||||
// Actor power-DC based scaling
|
||||
if ( save.scaling === "power" ) {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerdc") : null;
|
||||
}
|
||||
|
||||
// Assign labels
|
||||
this.labels = labels;
|
||||
// Ability-score based scaling
|
||||
else if ( save.scaling !== "flat" ) {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null;
|
||||
}
|
||||
|
||||
// Update labels
|
||||
const abl = CONFIG.SW5E.abilities[save.ability];
|
||||
this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl});
|
||||
return save.dc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -250,9 +280,251 @@ export default class Item5e extends Item {
|
|||
* @param {string} [rollMode] The roll display mode with which to display (or not) the card
|
||||
* @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return
|
||||
* the prepared chat message data (if false).
|
||||
* @return {Promise}
|
||||
* @return {Promise<ChatMessage|object|void>}
|
||||
*/
|
||||
async roll({configureDialog=true, rollMode=null, createMessage=true}={}) {
|
||||
async roll({configureDialog=true, rollMode, createMessage=true}={}) {
|
||||
let item = this;
|
||||
const actor = this.actor;
|
||||
|
||||
// Reference aspects of the item data necessary for usage
|
||||
const id = this.data.data; // Item data
|
||||
const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
|
||||
const resource = id.consume || {}; // Resource consumption
|
||||
const recharge = id.recharge || {}; // Recharge mechanic
|
||||
const uses = id?.uses ?? {}; // Limited uses
|
||||
const isPower = this.type === "power"; // Does the item require a power slot?
|
||||
const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Define follow-up actions resulting from the item usage
|
||||
let createMeasuredTemplate = hasArea; // Trigger a template creation
|
||||
let consumeRecharge = !!recharge.value; // Consume recharge
|
||||
let consumeResource = !!resource.target && (resource.type !== "ammo") // Consume a linked (non-ammo) resource
|
||||
let consumePowerSlot = requirePowerSlot; // Consume a power slot
|
||||
let consumeUsage = !!uses.per; // Consume limited uses
|
||||
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
|
||||
|
||||
// Display a configuration dialog to customize the usage
|
||||
const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage;
|
||||
if (configureDialog && needsConfiguration) {
|
||||
const configuration = await AbilityUseDialog.create(this);
|
||||
if (!configuration) return;
|
||||
|
||||
// Determine consumption preferences
|
||||
createMeasuredTemplate = Boolean(configuration.placeTemplate);
|
||||
consumeUsage = Boolean(configuration.consumeUse);
|
||||
consumeRecharge = Boolean(configuration.consumeRecharge);
|
||||
consumeResource = Boolean(configuration.consumeResource);
|
||||
consumePowerSlot = Boolean(configuration.consumeSlot);
|
||||
|
||||
// Handle power upcasting
|
||||
if ( requirePowerSlot ) {
|
||||
const slotLevel = configuration.level;
|
||||
const powerLevel = slotLevel === "pact" ? actor.data.data.powers.pact.level : parseInt(slotLevel);
|
||||
if (powerLevel !== id.level) {
|
||||
const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
|
||||
item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version
|
||||
}
|
||||
if ( consumePowerSlot ) consumePowerSlot = slotLevel === "pact" ? "pact" : `power${powerLevel}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the item can be used by testing for resource consumption
|
||||
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity});
|
||||
if ( !usage ) return;
|
||||
const {actorUpdates, itemUpdates, resourceUpdates} = usage;
|
||||
|
||||
// Commit pending data updates
|
||||
if ( !isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
|
||||
if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete();
|
||||
if ( !isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
|
||||
if ( !isObjectEmpty(resourceUpdates) ) {
|
||||
const resource = actor.items.get(id.consume?.target);
|
||||
if ( resource ) await resource.update(resourceUpdates);
|
||||
}
|
||||
|
||||
// Initiate measured template creation
|
||||
if ( createMeasuredTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview();
|
||||
}
|
||||
|
||||
// Create or return the Chat Message data
|
||||
return item.displayCard({rollMode, createMessage});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Verify that the consumed resources used by an Item are available.
|
||||
* Otherwise display an error and return false.
|
||||
* @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
|
||||
* @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
|
||||
* @param {boolean} consumeResource Whether the item consumes a limited resource
|
||||
* @param {string|boolean} consumePowerSlot A level of power slot consumed, or false
|
||||
* @param {boolean} consumeUsage Whether the item consumes a limited usage
|
||||
* @returns {object|boolean} A set of data changes to apply when the item is used, or false
|
||||
* @private
|
||||
*/
|
||||
_getUsageUpdates({consumeQuantity=false, consumeRecharge=false, consumeResource=false, consumePowerSlot=false, consumeUsage=false}) {
|
||||
|
||||
// Reference item data
|
||||
const id = this.data.data;
|
||||
const actorUpdates = {};
|
||||
const itemUpdates = {};
|
||||
const resourceUpdates = {};
|
||||
|
||||
// Consume Recharge
|
||||
if ( consumeRecharge ) {
|
||||
const recharge = id.recharge || {};
|
||||
if ( recharge.charged === false ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
itemUpdates["data.recharge.charged"] = false;
|
||||
}
|
||||
|
||||
// Consume Limited Resource
|
||||
if ( consumeResource ) {
|
||||
const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates);
|
||||
if ( canConsume === false ) return false;
|
||||
}
|
||||
|
||||
// Consume Power Slots
|
||||
if ( consumePowerSlot ) {
|
||||
const level = this.actor?.data.data.powers[consumePowerSlot];
|
||||
const powers = Number(level?.value ?? 0);
|
||||
if ( powers === 0 ) {
|
||||
const label = game.i18n.localize(consumePowerSlot === "pact" ? "SW5E.PowerProgPact" : `SW5E.PowerLevel${id.level}`);
|
||||
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
|
||||
return false;
|
||||
}
|
||||
actorUpdates[`data.powers.${consumePowerSlot}.value`] = Math.max(powers - 1, 0);
|
||||
}
|
||||
|
||||
// Consume Limited Usage
|
||||
if ( consumeUsage ) {
|
||||
const uses = id.uses || {};
|
||||
const available = Number(uses.value ?? 0);
|
||||
let used = false;
|
||||
|
||||
// Reduce usages
|
||||
const remaining = Math.max(available - 1, 0);
|
||||
if ( available >= 1 ) {
|
||||
used = true;
|
||||
itemUpdates["data.uses.value"] = remaining;
|
||||
}
|
||||
|
||||
// Otherwise reduce quantity
|
||||
if ( consumeQuantity && !used ) {
|
||||
const q = Number(id.quantity ?? 1);
|
||||
if ( q >= 1 ) {
|
||||
used = true;
|
||||
itemUpdates["data.quantity"] = Math.max(q - 1, 0);
|
||||
itemUpdates["data.uses.value"] = uses.max ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If the item was not used, return a warning
|
||||
if ( !used ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the configured usage
|
||||
return {itemUpdates, actorUpdates, resourceUpdates};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle update actions required when consuming an external resource
|
||||
* @param {object} itemUpdates An object of data updates applied to this item
|
||||
* @param {object} actorUpdates An object of data updates applied to the item owner (Actor)
|
||||
* @param {object} resourceUpdates An object of data updates applied to a different resource item (Item)
|
||||
* @return {boolean|void} Return false to block further progress, or return nothing to continue
|
||||
* @private
|
||||
*/
|
||||
_handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) {
|
||||
const actor = this.actor;
|
||||
const itemData = this.data.data;
|
||||
const consume = itemData.consume || {};
|
||||
if ( !consume.type ) return;
|
||||
|
||||
// No consumed target
|
||||
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
|
||||
if ( !consume.target ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Identify the consumed resource and its current quantity
|
||||
let resource = null;
|
||||
let amount = Number(consume.amount ?? 1);
|
||||
let quantity = 0;
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
resource = getProperty(actor.data.data, consume.target);
|
||||
quantity = resource || 0;
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
resource = actor.items.get(consume.target);
|
||||
quantity = resource ? resource.data.data.quantity : 0;
|
||||
break;
|
||||
case "charges":
|
||||
resource = actor.items.get(consume.target);
|
||||
if ( !resource ) break;
|
||||
const uses = resource.data.data.uses;
|
||||
if ( uses.per && uses.max ) quantity = uses.value;
|
||||
else if ( resource.data.data.recharge?.value ) {
|
||||
quantity = resource.data.data.recharge.charged ? 1 : 0;
|
||||
amount = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Verify that a consumed resource is available
|
||||
if ( !resource ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that the required quantity is available
|
||||
let remaining = quantity - amount;
|
||||
if ( remaining < 0 ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Define updates to provided data objects
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
actorUpdates[`data.${consume.target}`] = remaining;
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
resourceUpdates["data.quantity"] = remaining;
|
||||
break;
|
||||
case "charges":
|
||||
const uses = resource.data.data.uses || {};
|
||||
const recharge = resource.data.data.recharge || {};
|
||||
if ( uses.per && uses.max ) resourceUpdates["data.uses.value"] = remaining;
|
||||
else if ( recharge.value ) resourceUpdates["data.recharge.charged"] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display the chat card for an Item as a Chat Message
|
||||
* @param {object} options Options which configure the display of the item chat card
|
||||
* @param {string} rollMode The message visibility mode to apply to the created card
|
||||
* @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return
|
||||
* the prepared message data (if false)
|
||||
*/
|
||||
async displayCard({rollMode, createMessage=true}={}) {
|
||||
|
||||
// Basic template rendering data
|
||||
const token = this.actor.token;
|
||||
|
@ -271,190 +543,31 @@ export default class Item5e extends Item {
|
|||
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;
|
||||
} else if ( this.data.type === "consumable" ) {
|
||||
let configured = await this._rollConsumable(configureDialog);
|
||||
if ( configured === false ) return;
|
||||
}
|
||||
|
||||
// For items which consume a resource, handle that here
|
||||
const allowed = await this._handleResourceConsumption({isCard: true, isAttack: false});
|
||||
if ( allowed === false ) return;
|
||||
|
||||
// Render the chat card template
|
||||
const templateType = ["tool"].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
|
||||
// Create the ChatMessage data object
|
||||
const chatData = {
|
||||
user: game.user._id,
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
|
||||
content: html,
|
||||
flavor: this.data.data.chatFlavor || this.name,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
},
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor, token}),
|
||||
flags: {"core.canPopout": true}
|
||||
};
|
||||
|
||||
// If the consumable was destroyed in the process - embed the item data in the surviving message
|
||||
// If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
|
||||
if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) {
|
||||
chatData.flags["sw5e.itemData"] = this.data;
|
||||
}
|
||||
|
||||
// Toggle default roll mode
|
||||
rollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
|
||||
if ( rollMode === "blindroll" ) chatData["blind"] = true;
|
||||
// Apply the roll mode to adjust message visibility
|
||||
ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode"));
|
||||
|
||||
// Create the chat message
|
||||
if ( createMessage ) return ChatMessage.create(chatData);
|
||||
else return chatData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* For items which consume a resource, handle the consumption of that resource when the item is used.
|
||||
* There are four types of ability consumptions which are handled:
|
||||
* 1. Ammunition (on attack rolls)
|
||||
* 2. Attributes (on card usage)
|
||||
* 3. Materials (on card usage)
|
||||
* 4. Item Charges (on card usage)
|
||||
*
|
||||
* @param {boolean} isCard Is the item card being played?
|
||||
* @param {boolean} isAttack Is an attack roll being made?
|
||||
* @return {Promise<boolean>} Can the item card or attack roll be allowed to proceed?
|
||||
* @private
|
||||
*/
|
||||
async _handleResourceConsumption({isCard=false, isAttack=false}={}) {
|
||||
const itemData = this.data.data;
|
||||
const consume = itemData.consume || {};
|
||||
if ( !consume.type ) return true;
|
||||
const actor = this.actor;
|
||||
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
|
||||
|
||||
// Only handle certain types for certain actions
|
||||
if ( ((consume.type === "ammo") && !isAttack ) || ((consume.type !== "ammo") && !isCard) ) return true;
|
||||
|
||||
// No consumed target set
|
||||
if ( !consume.target ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Identify the consumed resource and it's quantity
|
||||
let consumed = null;
|
||||
let amount = parseInt(consume.amount || 1);
|
||||
let quantity = 0;
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
consumed = getProperty(actor.data.data, consume.target);
|
||||
quantity = consumed || 0;
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
consumed = actor.items.get(consume.target);
|
||||
quantity = consumed ? consumed.data.data.quantity : 0;
|
||||
break;
|
||||
case "charges":
|
||||
consumed = actor.items.get(consume.target);
|
||||
if ( !consumed ) break;
|
||||
const uses = consumed.data.data.uses;
|
||||
if ( uses.per && uses.max ) quantity = uses.value;
|
||||
else if ( consumed.data.data.recharge?.value ) {
|
||||
quantity = consumed.data.data.recharge.charged ? 1 : 0;
|
||||
amount = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Verify that the consumed resource is available
|
||||
if ( [null, undefined].includes(consumed) ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
let remaining = quantity - amount;
|
||||
if ( remaining < 0) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the consumed resource
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
await this.actor.update({[`data.${consume.target}`]: remaining});
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
await consumed.update({"data.quantity": remaining});
|
||||
break;
|
||||
case "charges":
|
||||
const uses = consumed.data.data.uses || {};
|
||||
const recharge = consumed.data.data.recharge || {};
|
||||
if ( uses.per && uses.max ) await consumed.update({"data.uses.value": remaining});
|
||||
else if ( recharge.value ) await consumed.update({"data.recharge.charged": false});
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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 charge = this.data.data.recharge;
|
||||
const uses = this.data.data.uses;
|
||||
let usesCharges = !!uses.per && !!uses.max;
|
||||
let placeTemplate = false;
|
||||
let consume = charge.value || 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("consumeUse"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
const current = getProperty(this.data, "data.uses.value") || 0;
|
||||
if ( consume && charge.value ) {
|
||||
if ( !charge.charged ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
else await this.update({"data.recharge.charged": false});
|
||||
}
|
||||
else if ( consume && usesCharges ) {
|
||||
if ( uses.value <= 0 ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
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();
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
// Create the Chat Message or return its data
|
||||
return createMessage ? ChatMessage.create(chatData) : chatData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -478,8 +591,9 @@ export default class Item5e extends Item {
|
|||
const fn = this[`_${this.data.type}ChatData`];
|
||||
if ( fn ) fn.bind(this)(data, labels, props);
|
||||
|
||||
// General equipment properties
|
||||
// Equipment properties
|
||||
if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) {
|
||||
if ( data.attunement === 1 ) props.push(game.i18n.localize(CONFIG.SW5E.attunements[1]));
|
||||
props.push(
|
||||
game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
|
||||
game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"),
|
||||
|
@ -614,7 +728,7 @@ export default class Item5e extends Item {
|
|||
|
||||
// Define Roll bonuses
|
||||
const parts = [`@mod`];
|
||||
if ( (this.data.type !== "weapon") || itemData.proficient ) {
|
||||
if ( !["weapon", "consumable"].includes(this.data.type) || itemData.proficient ) {
|
||||
parts.push("@prof");
|
||||
}
|
||||
|
||||
|
@ -625,9 +739,11 @@ export default class Item5e extends Item {
|
|||
|
||||
// Ammunition Bonus
|
||||
delete this._ammo;
|
||||
let ammo = null;
|
||||
let ammoUpdate = null;
|
||||
const consume = itemData.consume;
|
||||
if ( consume?.type === "ammo" ) {
|
||||
const ammo = this.actor.items.get(consume.target);
|
||||
ammo = this.actor.items.get(consume.target);
|
||||
if(ammo?.data){
|
||||
const q = ammo.data.data.quantity;
|
||||
const consumeAmount = consume.amount ?? 0;
|
||||
|
@ -641,6 +757,11 @@ export default class Item5e extends Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get pending ammunition update
|
||||
const usage = this._getUsageUpdates({consumeResource: true});
|
||||
if ( usage === false ) return null;
|
||||
ammoUpdate = usage.resourceUpdates || {};
|
||||
}
|
||||
|
||||
// Compose roll options
|
||||
|
@ -681,9 +802,8 @@ export default class Item5e extends Item {
|
|||
const roll = await d20Roll(rollConfig);
|
||||
if ( roll === false ) return null;
|
||||
|
||||
// Handle resource consumption if the attack roll was made
|
||||
const allowed = await this._handleResourceConsumption({isCard: false, isAttack: true});
|
||||
if ( allowed === false ) return null;
|
||||
// Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
|
||||
if ( ammo && !isObjectEmpty(ammoUpdate) ) await ammo.update(ammoUpdate);
|
||||
return roll;
|
||||
}
|
||||
|
||||
|
@ -693,12 +813,13 @@ export default class Item5e extends Item {
|
|||
* Place a damage roll using an item (weapon, feat, power, or equipment)
|
||||
* Rely upon the damageRoll logic for the core implementation.
|
||||
* @param {MouseEvent} [event] An event which triggered this roll, if any
|
||||
* @param {boolean} [critical] Should damage be rolled as a critical hit?
|
||||
* @param {number} [powerLevel] If the item is a power, override the level for damage scaling
|
||||
* @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula
|
||||
* @param {object} [options] Additional options passed to the damageRoll function
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
rollDamage({event, powerLevel=null, versatile=false, options={}}={}) {
|
||||
rollDamage({critical=false, event=null, powerLevel=null, versatile=false, options={}}={}) {
|
||||
if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item.");
|
||||
const itemData = this.data.data;
|
||||
const actorData = this.actor.data.data;
|
||||
|
@ -712,10 +833,12 @@ export default class Item5e extends Item {
|
|||
// Configure the damage roll
|
||||
const title = `${this.name} - ${game.i18n.localize("SW5E.DamageRoll")}`;
|
||||
const rollConfig = {
|
||||
event: event,
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
critical: critical ?? event?.altKey ?? false,
|
||||
data: rollData,
|
||||
event: event,
|
||||
fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false,
|
||||
parts: parts,
|
||||
title: title,
|
||||
flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
|
@ -864,74 +987,6 @@ export default class Item5e extends Item {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Use a consumable item, deducting from the quantity or charges of the item.
|
||||
* @param {boolean} configureDialog Whether to show a configuration dialog
|
||||
* @return {boolean} Whether further execution should be prevented
|
||||
* @private
|
||||
*/
|
||||
async _rollConsumable(configureDialog) {
|
||||
if ( this.data.type !== "consumable" ) throw new Error("Wrong Item type");
|
||||
const itemData = this.data.data;
|
||||
|
||||
// Determine whether to deduct uses of the item
|
||||
const uses = itemData.uses || {};
|
||||
const autoDestroy = uses.autoDestroy;
|
||||
let usesCharges = !!uses.per && (uses.max > 0);
|
||||
const recharge = itemData.recharge || {};
|
||||
const usesRecharge = !!recharge.value;
|
||||
|
||||
// Display a configuration dialog to confirm the usage
|
||||
let placeTemplate = false;
|
||||
let consume = uses.autoUse || true;
|
||||
if ( configureDialog ) {
|
||||
const usage = await AbilityUseDialog.create(this);
|
||||
if ( usage === null ) return false;
|
||||
consume = Boolean(usage.get("consumeUse"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
if ( consume ) {
|
||||
const current = uses.value || 0;
|
||||
const remaining = usesCharges ? Math.max(current - 1, 0) : current;
|
||||
if ( usesRecharge ) await this.update({"data.recharge.charged": false});
|
||||
else {
|
||||
const q = itemData.quantity;
|
||||
// Case 1, reduce charges
|
||||
if ( remaining ) {
|
||||
await this.update({"data.uses.value": remaining});
|
||||
}
|
||||
// Case 2, reduce quantity
|
||||
else if ( q > 1 ) {
|
||||
await this.update({"data.quantity": q - 1, "data.uses.value": uses.max || 0});
|
||||
}
|
||||
// Case 3, destroy the item
|
||||
else if ( (q <= 1) && autoDestroy ) {
|
||||
await this.actor.deleteOwnedItem(this.id);
|
||||
}
|
||||
// Case 4, reduce item to 0 quantity and 0 charges
|
||||
else if ( (q === 1) ) {
|
||||
await this.update({"data.quantity": q - 1, "data.uses.value": 0});
|
||||
}
|
||||
// Case 5, item unusable, display warning and do nothing
|
||||
else {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe initiate template placement workflow
|
||||
if ( this.hasAreaTarget && placeTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(this);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Perform an ability recharge test for an item which uses the d6 recharge mechanic
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
|
@ -1065,9 +1120,14 @@ export default class Item5e extends Item {
|
|||
case "attack":
|
||||
await item.rollAttack({event}); break;
|
||||
case "damage":
|
||||
await item.rollDamage({event, powerLevel}); break;
|
||||
case "versatile":
|
||||
await item.rollDamage({event, powerLevel, versatile: true}); break;
|
||||
await item.rollDamage({
|
||||
critical: event.altKey,
|
||||
event: event,
|
||||
powerLevel: powerLevel,
|
||||
versatile: action === "versatile"
|
||||
});
|
||||
break;
|
||||
case "formula":
|
||||
await item.rollFormula({event, powerLevel}); break;
|
||||
case "save":
|
||||
|
|
|
@ -61,6 +61,9 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
|
||||
// Original maximum uses formula
|
||||
if ( this.item._data.data?.uses?.max ) data.data.uses.max = this.item._data.data.uses.max;
|
||||
|
||||
// Vehicles
|
||||
data.isCrewed = data.item.data.activation?.type === 'crew';
|
||||
data.isMountable = this._isItemMountable(data.item);
|
||||
|
@ -91,7 +94,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||
}
|
||||
return ammo;
|
||||
}, {});
|
||||
}, {[item._id]: `${item.name} (${item.data.quantity})`});
|
||||
}
|
||||
|
||||
// Attributes
|
||||
|
@ -313,7 +316,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
// Render the Trait Selector dialog
|
||||
new TraitSelector(this.item, {
|
||||
name: a.dataset.edit,
|
||||
name: a.dataset.target,
|
||||
title: label.innerText,
|
||||
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
|
||||
if ( choices.includes(e[0] ) ) obj[e[0]] = e[1];
|
||||
|
|
|
@ -55,6 +55,5 @@ export function rollItemMacro(itemName) {
|
|||
const item = items[0];
|
||||
|
||||
// Trigger the item roll
|
||||
if ( item.data.type === "power" ) return actor.usePower(item);
|
||||
return item.roll();
|
||||
}
|
||||
|
|
|
@ -127,8 +127,8 @@ export const migrateActorData = function(actor) {
|
|||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
_migrateActorBonuses(actor, updateData);
|
||||
_migrateActorMovement(actor, updateData);
|
||||
_migrateActorSenses(actor, updateData);
|
||||
|
||||
// Migrate Owned Items
|
||||
if ( !actor.items ) return updateData;
|
||||
|
@ -191,6 +191,7 @@ function cleanActorData(actorData) {
|
|||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
_migrateItemAttunement(item, updateData);
|
||||
return updateData;
|
||||
};
|
||||
|
||||
|
@ -228,33 +229,72 @@ export const migrateSceneData = function(scene) {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor bonuses object
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorBonuses(actor, updateData) {
|
||||
const b = game.system.model.Actor.character.bonuses;
|
||||
for ( let k of Object.keys(actor.data.bonuses || {}) ) {
|
||||
if ( k in b ) updateData[`data.bonuses.${k}`] = b[k];
|
||||
else updateData[`data.bonuses.-=${k}`] = null;
|
||||
}
|
||||
function _migrateActorMovement(actor, updateData) {
|
||||
const ad = actor.data;
|
||||
const old = ad?.attributes?.speed?.value;
|
||||
if ( old === undefined ) return;
|
||||
const s = (old || "").split(" ");
|
||||
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
updateData["data.attributes.-=speed"] = null;
|
||||
return updateData
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor bonuses object
|
||||
* Migrate the actor traits.senses string to attributes.senses object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorMovement(actor, updateData) {
|
||||
if ( actor.data.attributes?.movement?.walk !== undefined ) return;
|
||||
const s = (actor.data.attributes?.speed?.value || "").split(" ");
|
||||
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
function _migrateActorSenses(actor, updateData) {
|
||||
const ad = actor.data;
|
||||
if ( ad?.traits?.senses === undefined ) return;
|
||||
const original = ad.traits.senses || "";
|
||||
|
||||
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
|
||||
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/
|
||||
let wasMatched = false;
|
||||
|
||||
// Match each comma-separated term
|
||||
for ( let s of original.split(",") ) {
|
||||
s = s.trim();
|
||||
const match = s.match(pattern);
|
||||
if ( !match ) continue;
|
||||
const type = match[1].toLowerCase();
|
||||
if ( type in CONFIG.SW5E.senses ) {
|
||||
updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
|
||||
wasMatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing was matched, but there was an old string - put the whole thing in "special"
|
||||
if ( !wasMatched && !!original ) {
|
||||
updateData["data.attributes.senses.special"] = original;
|
||||
}
|
||||
|
||||
// Remove the old traits.senses string once the migration is complete
|
||||
updateData["data.traits.-=senses"] = null;
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Delete the old data.attuned boolean
|
||||
* @private
|
||||
*/
|
||||
function _migrateItemAttunement(item, updateData) {
|
||||
if ( item.data.attuned === undefined ) return;
|
||||
updateData["data.attunement"] = 0;
|
||||
updateData["data.-=attuned"] = null;
|
||||
return updateData;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* A general tool to purge flags from all entities in a Compendium pack.
|
||||
* @param {Compendium} pack The compendium pack to clean
|
||||
|
|
|
@ -45,7 +45,10 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
}
|
||||
|
||||
// Return the template constructed from the item data
|
||||
return new this(templateData);
|
||||
const template = new this(templateData);
|
||||
template.item = item;
|
||||
template.actorSheet = item.actor?.sheet || null;
|
||||
return template;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -55,9 +58,16 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
*/
|
||||
drawPreview() {
|
||||
const initialLayer = canvas.activeLayer;
|
||||
|
||||
// Draw the template and switch to the template layer
|
||||
this.draw();
|
||||
this.layer.activate();
|
||||
this.layer.preview.addChild(this);
|
||||
|
||||
// Hide the sheet that originated the preview
|
||||
if ( this.actorSheet ) this.actorSheet.minimize();
|
||||
|
||||
// Activate interactivity
|
||||
this.activatePreviewListeners(initialLayer);
|
||||
}
|
||||
|
||||
|
@ -92,6 +102,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
canvas.app.view.oncontextmenu = null;
|
||||
canvas.app.view.onwheel = null;
|
||||
initialLayer.activate();
|
||||
this.actorSheet.maximize();
|
||||
};
|
||||
|
||||
// Confirm the workflow (left-click)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue