forked from GitHub-Mirrors/foundry-sw5e
Merge branch 'Develop' into professorbunbury-sw5e
This commit is contained in:
commit
9fea221d6b
113 changed files with 14433 additions and 6203 deletions
|
@ -6,7 +6,7 @@ import AbilityTemplate from "../pixi/ability-template.js";
|
|||
import {SW5E} from '../config.js';
|
||||
|
||||
/**
|
||||
* Extend the base Actor class to implement additional logic specialized for SW5e.
|
||||
* Extend the base Actor class to implement additional system-specific logic for SW5e.
|
||||
*/
|
||||
export default class Actor5e extends Actor {
|
||||
|
||||
|
@ -20,48 +20,6 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @override
|
||||
* TODO: This becomes unnecessary after 0.7.x is released
|
||||
*/
|
||||
initialize() {
|
||||
try {
|
||||
this.prepareData();
|
||||
} catch(err) {
|
||||
console.error(`Failed to initialize data for ${this.constructor.name} ${this.id}:`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @override
|
||||
* TODO: This becomes unnecessary after 0.7.x is released
|
||||
*/
|
||||
prepareData() {
|
||||
const is07x = !isNewerVersion("0.7.1", game.data.version);
|
||||
if ( is07x ) this.data = duplicate(this._data);
|
||||
if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
|
||||
if ( !this.data.name ) this.data.name = "New " + this.entity;
|
||||
this.prepareBaseData();
|
||||
this.prepareEmbeddedEntities();
|
||||
if ( is07x ) this.applyActiveEffects();
|
||||
this.prepareDerivedData();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @override
|
||||
* TODO: This becomes unnecessary after 0.7.x is released
|
||||
*/
|
||||
applyActiveEffects() {
|
||||
if (!isNewerVersion("0.7.1", game.data.version)) return super.applyActiveEffects();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
prepareBaseData() {
|
||||
switch ( this.data.type ) {
|
||||
|
@ -116,6 +74,11 @@ export default class Actor5e extends Actor {
|
|||
abl.save = Math.max(abl.save, originalSaves[id].save);
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory encumbrance
|
||||
data.attributes.encumbrance = this._computeEncumbrance(actorData);
|
||||
|
||||
// Prepare skills
|
||||
this._prepareSkills(actorData, bonuses, checkBonus, originalSkills);
|
||||
|
||||
// Determine Initiative Modifier
|
||||
|
@ -126,7 +89,8 @@ export default class Actor5e extends Actor {
|
|||
if ( joat ) init.prof = Math.floor(0.5 * data.attributes.prof);
|
||||
else if ( athlete ) init.prof = Math.ceil(0.5 * data.attributes.prof);
|
||||
else init.prof = 0;
|
||||
init.bonus = Number(init.value + (flags.initiativeAlert ? 5 : 0));
|
||||
init.value = init.value ?? 0;
|
||||
init.bonus = init.value + (flags.initiativeAlert ? 5 : 0);
|
||||
init.total = init.mod + init.prof + init.bonus;
|
||||
|
||||
// Prepare power-casting data
|
||||
|
@ -169,7 +133,7 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
return obj;
|
||||
}, {});
|
||||
data.prof = this.data.data.attributes.prof;
|
||||
data.prof = this.data.data.attributes.prof || 0;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -177,33 +141,36 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/**
|
||||
* Return the features which a character is awarded for each class level
|
||||
* @param cls {Object} Data object for class, equivalent to Item5e.data or raw compendium entry
|
||||
* @param {string} className The class name being added
|
||||
* @param {string} subclassName The subclass of the class being added, if any
|
||||
* @param {number} level The number of levels in the added class
|
||||
* @param {number} priorLevel The previous level of the added class
|
||||
* @return {Promise<Item5e[]>} Array of Item5e entities
|
||||
*/
|
||||
static async getClassFeatures(cls) {
|
||||
const level = cls.data.levels;
|
||||
const className = cls.name.toLowerCase();
|
||||
static async getClassFeatures({className="", subclassName="", level=1, priorLevel=0}={}) {
|
||||
className = className.toLowerCase();
|
||||
subclassName = subclassName.slugify();
|
||||
|
||||
// Get the configuration of features which may be added
|
||||
const clsConfig = CONFIG.SW5E.classFeatures[className];
|
||||
if (!clsConfig) return [];
|
||||
let featureIDs = clsConfig["features"][level] || [];
|
||||
const subclassName = cls.data.subclass.toLowerCase().slugify();
|
||||
if (!clsConfig) return [];
|
||||
|
||||
// Identify subclass features
|
||||
if ( subclassName !== "" ) {
|
||||
const subclassConfig = clsConfig["subclasses"][subclassName];
|
||||
if ( subclassConfig !== undefined ) {
|
||||
const subclassFeatureIDs = subclassConfig["features"][level];
|
||||
if ( subclassFeatureIDs ) {
|
||||
featureIDs = featureIDs.concat(subclassFeatureIDs);
|
||||
}
|
||||
}
|
||||
else console.warn("Invalid subclass: " + subclassName);
|
||||
// Acquire class features
|
||||
let ids = [];
|
||||
for ( let [l, f] of Object.entries(clsConfig.features || {}) ) {
|
||||
l = parseInt(l);
|
||||
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
|
||||
}
|
||||
|
||||
// Acquire subclass features
|
||||
const subConfig = clsConfig.subclasses[subclassName] || {};
|
||||
for ( let [l, f] of Object.entries(subConfig.features || {}) ) {
|
||||
l = parseInt(l);
|
||||
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
|
||||
}
|
||||
|
||||
// Load item data for all identified features
|
||||
const features = await Promise.all(featureIDs.map(id => fromUuid(id)));
|
||||
const features = await Promise.all(ids.map(id => fromUuid(id)));
|
||||
|
||||
// Class powers should always be prepared
|
||||
for ( const feature of features ) {
|
||||
|
@ -237,35 +204,28 @@ export default class Actor5e extends Actor {
|
|||
for (let u of updated instanceof Array ? updated : [updated]) {
|
||||
const item = this.items.get(u._id);
|
||||
if (!item || (item.data.type !== "class")) continue;
|
||||
const classData = duplicate(item.data);
|
||||
let changed = false;
|
||||
const updateData = expandObject(u);
|
||||
const config = {
|
||||
className: updateData.name || item.data.name,
|
||||
subclassName: updateData.data.subclass || item.data.data.subclass,
|
||||
level: getProperty(updateData, "data.levels"),
|
||||
priorLevel: item ? item.data.data.levels : 0
|
||||
}
|
||||
|
||||
// Get and create features for an increased class level
|
||||
const newLevels = getProperty(u, "data.levels");
|
||||
if (newLevels && (newLevels > item.data.data.levels)) {
|
||||
classData.data.levels = newLevels;
|
||||
changed = true;
|
||||
}
|
||||
let changed = false;
|
||||
if ( config.level && (config.level > config.priorLevel)) changed = true;
|
||||
if ( config.subclassName !== item.data.data.subclass ) changed = true;
|
||||
|
||||
// Get features for a newly changed subclass
|
||||
const newSubclass = getProperty(u, "data.subclass");
|
||||
if (newSubclass && (newSubclass !== item.data.data.subclass)) {
|
||||
classData.data.subclass = newSubclass;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Get the new features
|
||||
// Get features to create
|
||||
if ( changed ) {
|
||||
const features = await Actor5e.getClassFeatures(classData);
|
||||
if ( features.length ) toCreate.push(...features);
|
||||
const existing = new Set(this.items.map(i => i.name));
|
||||
const features = await Actor5e.getClassFeatures(config);
|
||||
for ( let f of features ) {
|
||||
if ( !existing.has(f.name) ) toCreate.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// De-dupe created items with ones that already exist (by name)
|
||||
if ( toCreate.length ) {
|
||||
const existing = new Set(this.items.map(i => i.name));
|
||||
toCreate = toCreate.filter(c => !existing.has(c.name));
|
||||
}
|
||||
return toCreate
|
||||
}
|
||||
|
||||
|
@ -301,9 +261,6 @@ export default class Actor5e extends Actor {
|
|||
const required = xp.max - prior;
|
||||
const pct = Math.round((xp.value - prior) * 100 / required);
|
||||
xp.pct = Math.clamped(pct, 0, 100);
|
||||
|
||||
// Inventory encumbrance
|
||||
data.attributes.encumbrance = this._computeEncumbrance(actorData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -369,17 +326,17 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
if ( joat && (skl.value === 0 ) ) multi = 0.5;
|
||||
|
||||
// Retain the maximum skill proficiency when skill proficiencies are merged
|
||||
if ( originalSkills ) {
|
||||
skl.value = Math.max(skl.value, originalSkills[id].value);
|
||||
}
|
||||
|
||||
// Compute modifier
|
||||
skl.bonus = checkBonus + skillBonus;
|
||||
skl.mod = data.abilities[skl.ability].mod;
|
||||
skl.prof = round(multi * data.attributes.prof);
|
||||
skl.total = skl.mod + skl.prof + skl.bonus;
|
||||
|
||||
// If we merged skills when transforming, take the highest bonus here.
|
||||
if (originalSkills && skl.value > 0.5) {
|
||||
skl.total = Math.max(skl.total, originalSkills[id].total);
|
||||
}
|
||||
|
||||
// Compute passive bonus
|
||||
const passive = observant && (feats.observantFeat.skills.includes(id)) ? 5 : 0;
|
||||
skl.passive = 10 + skl.total + passive;
|
||||
|
@ -489,8 +446,8 @@ export default class Actor5e extends Actor {
|
|||
else powers.pact.max = Math.max(1, Math.min(pl, 2), Math.min(pl - 8, 3), Math.min(pl - 13, 4));
|
||||
powers.pact.value = Math.min(powers.pact.value, powers.pact.max);
|
||||
} else {
|
||||
powers.pact.level = 0;
|
||||
powers.pact.max = 0;
|
||||
powers.pact.max = parseInt(powers.pact.override) || 0
|
||||
powers.pact.level = powers.pact.max > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -513,14 +470,14 @@ export default class Actor5e extends Actor {
|
|||
if ( !physicalItems.includes(i.type) ) return weight;
|
||||
const q = i.data.quantity || 0;
|
||||
const w = i.data.weight || 0;
|
||||
return weight + Math.round(q * w * 10) / 10;
|
||||
return weight + (q * w);
|
||||
}, 0);
|
||||
|
||||
// [Optional] add Currency Weight
|
||||
if ( game.settings.get("sw5e", "currencyWeight") ) {
|
||||
const currency = actorData.data.currency;
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0);
|
||||
weight += Math.round((numCoins * 10) / CONFIG.SW5E.encumbrance.currencyPerWeight) / 10;
|
||||
weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
|
||||
}
|
||||
|
||||
// Determine the encumbrance size class
|
||||
|
@ -535,9 +492,10 @@ export default class Actor5e extends Actor {
|
|||
if ( this.getFlag("sw5e", "powerfulBuild") ) mod = Math.min(mod * 2, 8);
|
||||
|
||||
// Compute Encumbrance percentage
|
||||
weight = weight.toNearest(0.1);
|
||||
const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod;
|
||||
const pct = Math.clamped((weight* 100) / max, 0, 100);
|
||||
return { value: weight, max, pct, encumbered: pct > (2/3) };
|
||||
const pct = Math.clamped((weight * 100) / max, 0, 100);
|
||||
return { value: weight.toNearest(0.1), max, pct, encumbered: pct > (2/3) };
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -564,9 +522,6 @@ export default class Actor5e extends Actor {
|
|||
/** @override */
|
||||
async update(data, options={}) {
|
||||
|
||||
// TODO: 0.7.1 compatibility - remove when stable
|
||||
if ( !data.hasOwnProperty("data") ) data = expandObject(data);
|
||||
|
||||
// Apply changes in Actor size to Token width/height
|
||||
const newSize = getProperty(data, "data.traits.size");
|
||||
if ( newSize && (newSize !== getProperty(this.data, "data.traits.size")) ) {
|
||||
|
@ -577,7 +532,7 @@ export default class Actor5e extends Actor {
|
|||
data["token.width"] = size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reset death save counters
|
||||
if ( (this.data.data.attributes.hp.value <= 0) && (getProperty(data, "data.attributes.hp.value") > 0) ) {
|
||||
setProperty(data, "data.attributes.death.success", 0);
|
||||
|
@ -858,7 +813,7 @@ export default class Actor5e extends Actor {
|
|||
rollAbilitySave(abilityId, options={}) {
|
||||
const label = CONFIG.SW5E.abilities[abilityId];
|
||||
const abl = this.data.data.abilities[abilityId];
|
||||
|
||||
|
||||
// Construct parts
|
||||
const parts = ["@mod"];
|
||||
const data = {mod: abl.mod};
|
||||
|
@ -937,7 +892,7 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Take action depending on the result
|
||||
const success = roll.total >= 10;
|
||||
const d20 = roll.dice[0].total;
|
||||
const d20 = roll.dice[0].total;
|
||||
|
||||
// Save success
|
||||
if ( success ) {
|
||||
|
@ -1429,14 +1384,16 @@ export default class Actor5e extends Actor {
|
|||
if ( !original ) return;
|
||||
|
||||
// Get the Tokens which represent this actor
|
||||
const tokens = this.getActiveTokens(true);
|
||||
const tokenUpdates = tokens.map(t => {
|
||||
const tokenData = duplicate(original.data.token);
|
||||
tokenData._id = t.id;
|
||||
tokenData.actorId = original.id;
|
||||
return tokenData;
|
||||
});
|
||||
canvas.scene.updateEmbeddedEntity("Token", tokenUpdates);
|
||||
if ( canvas.ready ) {
|
||||
const tokens = this.getActiveTokens(true);
|
||||
const tokenUpdates = tokens.map(t => {
|
||||
const tokenData = duplicate(original.data.token);
|
||||
tokenData._id = t.id;
|
||||
tokenData.actorId = original.id;
|
||||
return tokenData;
|
||||
});
|
||||
canvas.scene.updateEmbeddedEntity("Token", tokenUpdates);
|
||||
}
|
||||
|
||||
// Delete the polymorphed Actor and maybe re-render the original sheet
|
||||
const isRendered = this.sheet.rendered;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
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 {SW5E} from '../../config.js';
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet class to do all the SW5e things!
|
||||
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
|
||||
* This sheet is an Abstract layer which is not used.
|
||||
* @extends {ActorSheet}
|
||||
*/
|
||||
|
@ -43,8 +45,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/** @override */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/${this.actor.data.type}-sheet.html`;
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -94,6 +96,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
}
|
||||
|
||||
// Movement speeds
|
||||
data.movement = this._getMovementSpeed(data.actor);
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
|
||||
|
@ -101,7 +106,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
this._prepareItems(data);
|
||||
|
||||
// Prepare active effects
|
||||
this._prepareEffects(data);
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
|
@ -109,6 +114,28 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the display of movement speed data for the Actor
|
||||
* @param {object} actorData
|
||||
* @returns {{primary: string, special: string}}
|
||||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData) {
|
||||
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}`],
|
||||
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
|
||||
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
|
||||
].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
return {
|
||||
primary: `${movement.walk || 0} ${movement.units}`,
|
||||
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -147,43 +174,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the data structure for Active Effects which are currently applied to the Actor.
|
||||
* @param {object} data The object of rendering data which is being prepared
|
||||
* @private
|
||||
*/
|
||||
_prepareEffects(data) {
|
||||
|
||||
// Define effect header categories
|
||||
const categories = {
|
||||
temporary: {
|
||||
label: "Temporary Effects",
|
||||
effects: []
|
||||
},
|
||||
passive: {
|
||||
label: "Passive Effects",
|
||||
effects: []
|
||||
},
|
||||
inactive: {
|
||||
label: "Inactive Effects",
|
||||
effects: []
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over active effects, classifying them into categories
|
||||
for ( let e of this.actor.effects ) {
|
||||
e._getSourceName(); // Trigger a lookup for the source name
|
||||
if ( e.data.disabled ) categories.inactive.effects.push(e);
|
||||
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
||||
else categories.passive.effects.push(e);
|
||||
}
|
||||
|
||||
// Add the prepared categories of effects to the rendering data
|
||||
return data.effects = categories;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Insert a power into the powerbook object when rendering the character sheet
|
||||
* @param {Object} data The Actor data being prepared
|
||||
|
@ -242,7 +232,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
|
@ -363,7 +353,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
|
||||
|
||||
// Item summaries
|
||||
html.find('.item .item-name h4').click(event => this._onItemSummary(event));
|
||||
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
|
||||
|
||||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
@ -383,6 +373,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));
|
||||
|
||||
// Owned Item management
|
||||
|
@ -393,8 +384,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(this._onManageActiveEffect.bind(this));
|
||||
|
||||
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
|
||||
}
|
||||
|
||||
// Owner Only Listeners
|
||||
|
@ -565,7 +555,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
|
@ -576,9 +566,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// Create the owned item as normal
|
||||
// TODO remove conditional logic in 0.7.x
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData);
|
||||
else return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -731,28 +719,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
|
||||
* @param {MouseEvent} event The left-click event on the effect control
|
||||
* @private
|
||||
*/
|
||||
_onManageActiveEffect(event) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const li = a.closest(".effect");
|
||||
const effect = this.actor.effects.get(li.dataset.effectId);
|
||||
switch ( a.dataset.action ) {
|
||||
case "edit":
|
||||
return effect.sheet.render(true);
|
||||
case "delete":
|
||||
return effect.delete();
|
||||
case "toggle":
|
||||
return effect.update({disabled: !effect.data.disabled});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling an Ability check, either a test or a saving throw
|
||||
* @param {Event} event The originating click event
|
||||
|
@ -825,6 +791,18 @@ 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();
|
||||
|
@ -839,90 +817,4 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
});
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* DEPRECATED */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* TODO: Remove once 0.7.x is release
|
||||
* @deprecated since 0.7.0
|
||||
*/
|
||||
async _onDrop (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Get dropped data
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.dataTransfer.getData('text/plain'));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
if ( !data ) return false;
|
||||
|
||||
// Handle the drop with a Hooked function
|
||||
const allowed = Hooks.call("dropActorSheetData", this.actor, this, data);
|
||||
if ( allowed === false ) return;
|
||||
|
||||
// Case 1 - Dropped Item
|
||||
if ( data.type === "Item" ) {
|
||||
return this._onDropItem(event, data);
|
||||
}
|
||||
|
||||
// Case 2 - Dropped Actor
|
||||
if ( data.type === "Actor" ) {
|
||||
return this._onDropActor(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* TODO: Remove once 0.7.x is release
|
||||
* @deprecated since 0.7.0
|
||||
*/
|
||||
async _onDropItem(event, data) {
|
||||
if ( !this.actor.owner ) return false;
|
||||
let itemData = await this._getItemDropData(event, data);
|
||||
|
||||
// Handle item sorting within the same Actor
|
||||
const actor = this.actor;
|
||||
let sameActor = (data.actorId === actor._id) || (actor.isToken && (data.tokenId === actor.token.id));
|
||||
if (sameActor) return this._onSortItem(event, itemData);
|
||||
|
||||
// Create a new item
|
||||
this._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* TODO: Remove once 0.7.x is release
|
||||
* @deprecated since 0.7.0
|
||||
*/
|
||||
async _getItemDropData(event, data) {
|
||||
let itemData = null;
|
||||
|
||||
// Case 1 - Import from a Compendium pack
|
||||
if (data.pack) {
|
||||
const pack = game.packs.get(data.pack);
|
||||
if (pack.metadata.entity !== "Item") return;
|
||||
itemData = await pack.getEntry(data.id);
|
||||
}
|
||||
|
||||
// Case 2 - Data explicitly provided
|
||||
else if (data.data) {
|
||||
itemData = data.data;
|
||||
}
|
||||
|
||||
// Case 3 - Import from World entity
|
||||
else {
|
||||
let item = game.items.get(data.id);
|
||||
if (!item) return;
|
||||
itemData = item.data;
|
||||
}
|
||||
|
||||
// Return a copy of the extracted data
|
||||
return duplicate(itemData);
|
||||
}
|
||||
}
|
631
module/actor/sheets/newSheet/character.js
Normal file
631
module/actor/sheets/newSheet/character.js
Normal file
|
@ -0,0 +1,631 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
||||
|
||||
get template() {
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/newActor/character-sheet.html";
|
||||
}
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["swalt", "sw5e", "sheet", "actor", "character"],
|
||||
blockFavTab: true,
|
||||
subTabs: null,
|
||||
width: 800,
|
||||
tabs: [{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const sheetData = super.getData();
|
||||
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Resources
|
||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
}, []);
|
||||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" ) arr[1].push(item);
|
||||
else if ( item.type === "feat" ) arr[2].push(item);
|
||||
else if ( item.type === "class" ) arr[3].push(item);
|
||||
else if ( item.type === "species" ) arr[4].push(item);
|
||||
else if ( item.type === "archetype" ) arr[5].push(item);
|
||||
else if ( item.type === "classfeature" ) arr[6].push(item);
|
||||
else if ( item.type === "background" ) arr[7].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[8].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], [], [], [], [], [], []]);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for ( let i of items ) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter(s => {
|
||||
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
lightsaberform: { label: "SW5E.ItemTypeLightsaberForm", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
};
|
||||
for ( let f of feats ) {
|
||||
if ( f.data.activation.type ) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.lightsaberform.items = lightsaberforms;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.powerbook = powerbook;
|
||||
data.preparedPowers = nPrepared;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to establish the displayed preparation state for an item
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_prepareItemToggleState(item) {
|
||||
if (item.type === "power") {
|
||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if ( isAlways ) item.toggleClass = "fixed";
|
||||
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
}
|
||||
else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if ( !this.options.editable ) return;
|
||||
|
||||
// Inventory Functions
|
||||
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// Item State Toggling
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
|
||||
// Short and Long Rest
|
||||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
|
||||
// Death saving throws
|
||||
html.find('.death-save').click(this._onDeathSave.bind(this));
|
||||
|
||||
// Send Languages to Chat onClick
|
||||
html.find('[data-options="share-languages"]').click(event => {
|
||||
event.preventDefault();
|
||||
let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", ");
|
||||
let custom = this.actor.data.data.traits.languages.custom;
|
||||
if (custom) langs += ", " + custom.replace(/;/g, ",");
|
||||
let content = `
|
||||
<div class="sw5e chat-card item-card" data-acor-id="${this.actor._id}">
|
||||
<header class="card-header flexrow">
|
||||
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
|
||||
<h3>Known Languages</h3>
|
||||
</header>
|
||||
<div class="card-content">${langs}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Send to Chat
|
||||
let rollWhisper = null;
|
||||
let rollBlind = false;
|
||||
let rollMode = game.settings.get("core", "rollMode");
|
||||
if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM");
|
||||
if (rollMode === "blindroll") rollBlind = true;
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
content: content,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
},
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER
|
||||
});
|
||||
});
|
||||
|
||||
// Item Delete Confirmation
|
||||
html.find('.item-delete').off("click");
|
||||
html.find('.item-delete').click(event => {
|
||||
let li = $(event.currentTarget).parents('.item');
|
||||
let itemId = li.attr("data-item-id");
|
||||
let item = this.actor.getOwnedItem(itemId);
|
||||
new Dialog({
|
||||
title: `Deleting ${item.data.name}`,
|
||||
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
|
||||
buttons: {
|
||||
Yes: {
|
||||
icon: '<i class="fa fa-check"></i>',
|
||||
label: 'Yes',
|
||||
callback: dlg => {
|
||||
this.actor.deleteOwnedItem(itemId);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: 'No'
|
||||
},
|
||||
},
|
||||
default: 'cancel'
|
||||
}).render(true);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a death saving throw for the Character
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onDeathSave(event) {
|
||||
event.preventDefault();
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Handle toggling the state of an Owned Item within the Actor
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a short rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onShortRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.shortRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a long rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onLongRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.longRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events to convert currency to the highest possible denomination
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onConvertCurrency(event) {
|
||||
event.preventDefault();
|
||||
return Dialog.confirm({
|
||||
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
||||
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
||||
yes: () => this.actor.convertCurrency()
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Upgrade the number of class levels a character has and add features
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const classWasAlreadyPresent = !!cls;
|
||||
|
||||
// Add new features for class level
|
||||
if ( !classWasAlreadyPresent ) {
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
}
|
||||
|
||||
// If the actor already has the class, increment the level instead of creating a new item
|
||||
// then add new features as long as level increases
|
||||
if ( classWasAlreadyPresent ) {
|
||||
const lvl = cls.data.data.levels;
|
||||
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
|
||||
if ( !(lvl === newLvl) ) {
|
||||
cls.update({"data.levels": newLvl});
|
||||
itemData.data.levels = newLvl;
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
||||
async function addFavorites(app, html, data) {
|
||||
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
|
||||
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
|
||||
// It is licensed under a Creative Commons Attribution 4.0 International License
|
||||
// and can be found at https://github.com/syl3r86/favtab.
|
||||
let favItems = [];
|
||||
let favFeats = [];
|
||||
let favPowers = {
|
||||
0: {
|
||||
isCantrip: true,
|
||||
powers: []
|
||||
},
|
||||
1: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power1.value,
|
||||
max: data.actor.data.powers.power1.max
|
||||
},
|
||||
2: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power2.value,
|
||||
max: data.actor.data.powers.power2.max
|
||||
},
|
||||
3: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power3.value,
|
||||
max: data.actor.data.powers.power3.max
|
||||
},
|
||||
4: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power4.value,
|
||||
max: data.actor.data.powers.power4.max
|
||||
},
|
||||
5: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power5.value,
|
||||
max: data.actor.data.powers.power5.max
|
||||
},
|
||||
6: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power6.value,
|
||||
max: data.actor.data.powers.power6.max
|
||||
},
|
||||
7: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power7.value,
|
||||
max: data.actor.data.powers.power7.max
|
||||
},
|
||||
8: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power8.value,
|
||||
max: data.actor.data.powers.power8.max
|
||||
},
|
||||
9: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power9.value,
|
||||
max: data.actor.data.powers.power9.max
|
||||
}
|
||||
}
|
||||
|
||||
let powerCount = 0
|
||||
let items = data.actor.items;
|
||||
for (let item of items) {
|
||||
if (item.type == "class") continue;
|
||||
if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) {
|
||||
item.flags.favtab = {
|
||||
isFavourite: false
|
||||
};
|
||||
}
|
||||
let isFav = item.flags.favtab.isFavourite;
|
||||
if (app.options.editable) {
|
||||
let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`);
|
||||
favBtn.click(ev => {
|
||||
app.actor.getOwnedItem(item._id).update({
|
||||
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite
|
||||
});
|
||||
});
|
||||
html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn);
|
||||
}
|
||||
|
||||
if (isFav) {
|
||||
item.powerComps = "";
|
||||
if (item.data.components) {
|
||||
let comps = item.data.components;
|
||||
let v = (comps.vocal) ? "V" : "";
|
||||
let s = (comps.somatic) ? "S" : "";
|
||||
let m = (comps.material) ? "M" : "";
|
||||
let c = (comps.concentration) ? true : false;
|
||||
let r = (comps.ritual) ? true : false;
|
||||
item.powerComps = `${v}${s}${m}`;
|
||||
item.powerCon = c;
|
||||
item.powerRit = r;
|
||||
}
|
||||
|
||||
item.editable = app.options.editable;
|
||||
switch (item.type) {
|
||||
case 'feat':
|
||||
if (item.flags.favtab.sort === undefined) {
|
||||
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
|
||||
}
|
||||
favFeats.push(item);
|
||||
break;
|
||||
case 'power':
|
||||
if (item.data.preparation.mode) {
|
||||
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`
|
||||
}
|
||||
if (item.data.level) {
|
||||
favPowers[item.data.level].powers.push(item);
|
||||
} else {
|
||||
favPowers[0].powers.push(item);
|
||||
}
|
||||
powerCount++;
|
||||
break;
|
||||
default:
|
||||
if (item.flags.favtab.sort === undefined) {
|
||||
item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present
|
||||
}
|
||||
favItems.push(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alter core CSS to fit new button
|
||||
// if (app.options.editable) {
|
||||
// html.find('.powerbook .item-controls').css('flex', '0 0 88px');
|
||||
// html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px');
|
||||
// html.find('.favourite .item-controls').css('flex', '0 0 22px');
|
||||
// }
|
||||
|
||||
let tabContainer = html.find('.favtabtarget');
|
||||
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
|
||||
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
|
||||
data.favPowers = powerCount > 0 ? favPowers : false;
|
||||
data.editable = app.options.editable;
|
||||
|
||||
await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']);
|
||||
let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data));
|
||||
favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event));
|
||||
|
||||
if (app.options.editable) {
|
||||
favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
|
||||
let handler = ev => app._onDragItemStart(ev);
|
||||
favtabHtml.find('.item').each((i, li) => {
|
||||
if (li.classList.contains("inventory-header")) return;
|
||||
li.setAttribute("draggable", true);
|
||||
li.addEventListener("dragstart", handler, false);
|
||||
});
|
||||
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
|
||||
favtabHtml.find('.item-edit').click(ev => {
|
||||
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
|
||||
app.actor.getOwnedItem(itemId).sheet.render(true);
|
||||
});
|
||||
favtabHtml.find('.item-fav').click(ev => {
|
||||
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
|
||||
let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite
|
||||
app.actor.getOwnedItem(itemId).update({
|
||||
"flags.favtab.isFavourite": val
|
||||
});
|
||||
});
|
||||
|
||||
// Sorting
|
||||
favtabHtml.find('.item').on('drop', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain'));
|
||||
// if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return;
|
||||
if (dropData.actorId !== app.actor.id) return;
|
||||
let list = null;
|
||||
if (dropData.data.type === 'feat') list = favFeats;
|
||||
else list = favItems;
|
||||
let dragSource = list.find(i => i._id === dropData.data._id);
|
||||
let siblings = list.filter(i => i._id !== dropData.data._id);
|
||||
let targetId = ev.target.closest('.item').dataset.itemId;
|
||||
let dragTarget = siblings.find(s => s._id === targetId);
|
||||
|
||||
if (dragTarget === undefined) return;
|
||||
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
|
||||
target: dragTarget,
|
||||
siblings: siblings,
|
||||
sortKey: 'flags.favtab.sort'
|
||||
});
|
||||
const updateData = sortUpdates.map(u => {
|
||||
const update = u.update;
|
||||
update._id = u.target._id;
|
||||
return update;
|
||||
});
|
||||
app.actor.updateEmbeddedEntity("OwnedItem", updateData);
|
||||
});
|
||||
}
|
||||
tabContainer.append(favtabHtml);
|
||||
|
||||
// try {
|
||||
// if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div");
|
||||
// }
|
||||
// catch (err) {
|
||||
// // Better Rolls not found!
|
||||
// }
|
||||
Hooks.callAll("renderedSwaltSheet", app, html, data);
|
||||
}
|
||||
async function addSubTabs(app, html, data) {
|
||||
if(data.options.subTabs == null) {
|
||||
//let subTabs = []; //{subgroup: '', target: '', active: false}
|
||||
data.options.subTabs = {};
|
||||
html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => {
|
||||
let subgroup = el.getAttribute('data-subgroup');
|
||||
let target = el.getAttribute('data-target');
|
||||
let targetObj = {target: target, active: el.classList.contains("active")}
|
||||
if(data.options.subTabs.hasOwnProperty(subgroup)) {
|
||||
data.options.subTabs[subgroup].push(targetObj);
|
||||
} else {
|
||||
data.options.subTabs[subgroup] = [];
|
||||
data.options.subTabs[subgroup].push(targetObj);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for(const group in data.options.subTabs) {
|
||||
data.options.subTabs[group].forEach(tab => {
|
||||
if(tab.active) {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active');
|
||||
} else {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
html.find('[data-subgroup-selection]').children().on('click', event => {
|
||||
let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup');
|
||||
let target = event.target.closest('[data-target]').getAttribute('data-target');
|
||||
html.find(`[data-subgroup=${subgroup}]`).removeClass('active');
|
||||
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active');
|
||||
let tabId = data.options.subTabs[subgroup].find(tab => {
|
||||
return tab.target == target
|
||||
});
|
||||
data.options.subTabs[subgroup].map(el => {
|
||||
if(el.target == target) {
|
||||
el.active = true;
|
||||
} else {
|
||||
el.active = false;
|
||||
}
|
||||
return el;
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {
|
||||
addFavorites(app, html, data);
|
||||
addSubTabs(app, html, data);
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../entity.js";
|
||||
import ActorSheet5e from "../base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the SW5E system.
|
||||
|
@ -68,7 +68,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
};
|
||||
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
|
@ -109,7 +109,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
for ( let i of items ) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
|
@ -123,12 +123,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
||||
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
||||
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
||||
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
||||
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
};
|
||||
|
@ -138,13 +138,13 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
features.fightingmasteries.items = fightingmasteries;
|
||||
features.lightsaberforms.items = lightsaberforms;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
features.fightingmasteries.items = fightingmasteries;
|
||||
features.lightsaberforms.items = lightsaberforms;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
|
@ -189,9 +189,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
super.activateListeners(html);
|
||||
if ( !this.options.editable ) return;
|
||||
|
||||
// Inventory Functions
|
||||
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// Item State Toggling
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
|
||||
|
@ -199,8 +196,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
|
||||
// Death saving throws
|
||||
html.find('.death-save').click(this._onDeathSave.bind(this));
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -210,14 +207,19 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onDeathSave(event) {
|
||||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
const button = event.currentTarget;
|
||||
switch( button.dataset.action ) {
|
||||
case "rollDeathSave":
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
case "rollInitiative":
|
||||
return this.actor.rollInitiative({createCombatants: true});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Handle toggling the state of an Owned Item within the Actor
|
||||
* @param {Event} event The triggering click event
|
||||
|
@ -259,53 +261,39 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events to convert currency to the highest possible denomination
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onConvertCurrency(event) {
|
||||
event.preventDefault();
|
||||
return Dialog.confirm({
|
||||
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
||||
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
||||
yes: () => this.actor.convertCurrency()
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
let addLevel = false;
|
||||
|
||||
// Upgrade the number of class levels a character has and add features
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const classWasAlreadyPresent = !!cls;
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
const hasClass = !!cls;
|
||||
|
||||
// Add new features for class level
|
||||
if ( !classWasAlreadyPresent ) {
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
// Increment levels instead of creating a new item
|
||||
if ( hasClass ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// If the actor already has the class, increment the level instead of creating a new item
|
||||
// then add new features as long as level increases
|
||||
if ( classWasAlreadyPresent ) {
|
||||
const lvl = cls.data.data.levels;
|
||||
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
|
||||
if ( !(lvl === newLvl) ) {
|
||||
cls.update({"data.levels": newLvl});
|
||||
itemData.data.levels = newLvl;
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
}
|
||||
return
|
||||
// Add class features
|
||||
if ( !hasClass || addLevel ) {
|
||||
const features = await Actor5e.getClassFeatures({
|
||||
className: itemData.name,
|
||||
subclassName: itemData.data.subclass,
|
||||
level: itemData.levels,
|
||||
priorLevel: priorLevel
|
||||
});
|
||||
await this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
}
|
||||
}
|
||||
|
||||
super._onDropItemCreate(itemData);
|
||||
// Default drop handling if levels were not added
|
||||
if ( !addLevel ) super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../sheets/base.js";
|
||||
import ActorSheet5e from "../base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the SW5E system.
|
||||
|
@ -106,7 +106,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
|
||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -116,7 +116,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
* @param {Event} event The original click event
|
||||
* @private
|
||||
*/
|
||||
_onRollHealthFormula(event) {
|
||||
_onRollHPFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
import ActorSheet5e from "../base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for Vehicle type actors.
|
||||
|
@ -49,12 +49,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
|
||||
|
||||
// Compute overall encumbrance
|
||||
const enc = {
|
||||
max: actorData.data.attributes.capacity.cargo,
|
||||
value: Math.round(totalWeight * 10) / 10
|
||||
};
|
||||
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
|
||||
return enc;
|
||||
const max = actorData.data.attributes.capacity.cargo;
|
||||
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
|
||||
return {value: totalWeight.toNearest(0.1), max, pct};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -89,6 +86,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData) {
|
||||
return {primary: "", special: ""};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the Vehicle sheet.
|
||||
* @private
|
|
@ -70,7 +70,7 @@ export default class AbilityUseDialog extends Dialog {
|
|||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* An application class which provides advanced configuration for special character flags which modify an Actor
|
||||
* @extends {BaseEntitySheet}
|
||||
* @implements {BaseEntitySheet}
|
||||
*/
|
||||
export default class ActorSheetFlags extends BaseEntitySheet {
|
||||
static get defaultOptions() {
|
||||
static get defaultOptions() {
|
||||
const options = super.defaultOptions;
|
||||
return mergeObject(options, {
|
||||
id: "actor-flags",
|
||||
|
@ -16,22 +16,16 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configure the title of the special traits selection window to include the Actor name
|
||||
* @type {String}
|
||||
*/
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare data used to render the special Actor traits selection UI
|
||||
* @return {Object}
|
||||
*/
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
const data = {};
|
||||
data.actor = this.object;
|
||||
data.flags = this._getFlags();
|
||||
data.bonuses = this._getBonuses();
|
||||
|
@ -43,17 +37,18 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
/**
|
||||
* Prepare an object of flags data which groups flags by section
|
||||
* Add some additional data for rendering
|
||||
* @return {Object}
|
||||
* @return {object}
|
||||
*/
|
||||
_getFlags() {
|
||||
const flags = {};
|
||||
const baseData = this.entity._data;
|
||||
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
|
||||
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
|
||||
let flag = duplicate(v);
|
||||
flag.type = v.type.name;
|
||||
flag.isCheckbox = v.type === Boolean;
|
||||
flag.isSelect = v.hasOwnProperty('choices');
|
||||
flag.value = this.entity.getFlag("sw5e", k);
|
||||
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
|
||||
flags[v.section][`flags.sw5e.${k}`] = flag;
|
||||
}
|
||||
return flags;
|
||||
|
@ -63,7 +58,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
|
||||
/**
|
||||
* Get the bonuses fields and their localization strings
|
||||
* @return {Array}
|
||||
* @return {Array<object>}
|
||||
* @private
|
||||
*/
|
||||
_getBonuses() {
|
||||
|
@ -82,17 +77,14 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}
|
||||
];
|
||||
for ( let b of bonuses ) {
|
||||
b.value = getProperty(this.object.data, b.name) || "";
|
||||
b.value = getProperty(this.object._data, b.name) || "";
|
||||
}
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the Actor using the configured flags
|
||||
* Remove/unset any flags which are no longer configured
|
||||
*/
|
||||
/** @override */
|
||||
async _updateObject(event, formData) {
|
||||
const actor = this.object;
|
||||
let updateData = expandObject(formData);
|
||||
|
@ -100,10 +92,12 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
// Unset any flags which are "false"
|
||||
let unset = false;
|
||||
const flags = updateData.flags.sw5e;
|
||||
//clone flags to dnd5e for module compatability
|
||||
updateData.flags.dnd5e = updateData.flags.sw5e
|
||||
for ( let [k, v] of Object.entries(flags) ) {
|
||||
if ( [undefined, null, "", false, 0].includes(v) ) {
|
||||
delete flags[k];
|
||||
if ( hasProperty(actor.data.flags, `sw5e.${k}`) ) {
|
||||
if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) {
|
||||
unset = true;
|
||||
flags[`-=${k}`] = null;
|
||||
}
|
||||
|
@ -118,10 +112,6 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
}
|
||||
|
||||
// Diff the data against any applied overrides and apply
|
||||
// TODO: Remove this logical gate once 0.7.x is release channel
|
||||
if ( !isNewerVersion("0.7.1", game.data.version) ){
|
||||
updateData = diffObject(this.object.data, updateData);
|
||||
}
|
||||
await actor.update(updateData, {diff: false});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/**
|
||||
* A specialized Dialog subclass for casting a cast item at a certain level
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export class CastDialog extends Dialog {
|
||||
constructor(actor, item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
||||
/**
|
||||
* Store a reference to the Actor entity which is casting the cast
|
||||
* @type {Actor5e}
|
||||
*/
|
||||
this.actor = actor;
|
||||
|
||||
/**
|
||||
* Store a reference to the Item entity which is the cast being cast
|
||||
* @type {Item5e}
|
||||
*/
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A constructor function which displays the Cast Cast Dialog app for a given Actor and Item.
|
||||
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
|
||||
* @param {Actor5e} actor
|
||||
* @param {Item5e} item
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async create(actor, item) {
|
||||
const ad = actor.data.data;
|
||||
const id = item.data.data;
|
||||
|
||||
// Determine whether the cast may be upcast
|
||||
const lvl = id.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.castUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const castLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const l = ad.casts["cast"+i] || {max: 0, override: null};
|
||||
let max = parseInt(l.override || l.max || 0);
|
||||
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? `${CONFIG.SW5E.castLevels[i]} (${slots} Slots)` : CONFIG.SW5E.castLevels[i],
|
||||
canCast: canUpcast && (max > 0),
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
|
||||
const pact = ad.casts.pact;
|
||||
if (pact.level >= lvl) {
|
||||
// If this character has pact slots, present them as an option for
|
||||
// casting the cast.
|
||||
castLevels.push({
|
||||
level: 'pact',
|
||||
label: game.i18n.localize('SW5E.CastLevelPact')
|
||||
+ ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) `
|
||||
+ `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`,
|
||||
canCast: canUpcast,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
|
||||
const canCast = castLevels.some(l => l.hasSlots);
|
||||
|
||||
// Render the Cast casting template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/cast-cast.html", {
|
||||
item: item.data,
|
||||
canCast: canCast,
|
||||
canUpcast: canUpcast,
|
||||
castLevels,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget
|
||||
});
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, item, {
|
||||
title: `${item.name}: Cast Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
cast: {
|
||||
icon: '<i class="fas fa-magic"></i>',
|
||||
label: "Cast",
|
||||
callback: html => resolve(new FormData(html[0].querySelector("#cast-config-form")))
|
||||
}
|
||||
},
|
||||
default: "cast",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
32
module/apps/movement-config.js
Normal file
32
module/apps/movement-config.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* A simple form to set actor movement speeds
|
||||
* @implements {BaseEntitySheet}
|
||||
*/
|
||||
export default class MovementConfig 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,
|
||||
height: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const data = {
|
||||
movement: duplicate(this.entity._data.data.attributes.movement),
|
||||
units: CONFIG.SW5E.movementUnits
|
||||
}
|
||||
for ( let [k, v] of Object.entries(data.movement) ) {
|
||||
if ( ["units", "hover"].includes(k) ) continue;
|
||||
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ export default class ShortRestDialog extends Dialog {
|
|||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
|
||||
// Determine Hit Dice
|
||||
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
||||
if ( item.type === "class" ) {
|
||||
|
@ -49,7 +49,7 @@ export default class ShortRestDialog extends Dialog {
|
|||
}, {});
|
||||
data.canRoll = this.actor.data.data.attributes.hd > 0;
|
||||
data.denomination = this._denom;
|
||||
|
||||
|
||||
// Determine rest type
|
||||
const variant = game.settings.get("sw5e", "restVariant");
|
||||
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
|
||||
|
|
|
@ -11,14 +11,16 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
|
|||
const d = roll.dice[0];
|
||||
|
||||
// Ensure it is an un-modified d20 roll
|
||||
const isD20 = (d.faces === 20) && ( d.results.length === 1 );
|
||||
const isD20 = (d.faces === 20) && ( d.values.length === 1 );
|
||||
if ( !isD20 ) return;
|
||||
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
|
||||
if ( isModifiedRoll ) return;
|
||||
|
||||
// Highlight successes and failures
|
||||
if ( d.options.critical && (d.total >= d.options.critical) ) html.find(".dice-total").addClass("critical");
|
||||
else if ( d.options.fumble && (d.total <= d.options.fumble) ) html.find(".dice-total").addClass("fumble");
|
||||
const critical = d.options.critical || 20;
|
||||
const fumble = d.options.fumble || 1;
|
||||
if ( d.total >= critical ) html.find(".dice-total").addClass("critical");
|
||||
else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble");
|
||||
else if ( d.options.target ) {
|
||||
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
|
||||
else html.find(".dice-total").addClass("failure");
|
||||
|
@ -33,7 +35,8 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
|
|||
export const displayChatActionButtons = function(message, html, data) {
|
||||
const chatCard = html.find(".sw5e.chat-card");
|
||||
if ( chatCard.length > 0 ) {
|
||||
html.find(".flavor-text").remove();
|
||||
const flavor = html.find(".flavor-text");
|
||||
if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
|
||||
|
||||
// If the user is the message author or the actor owner, proceed
|
||||
let actor = game.actors.get(data.message.speaker.actor);
|
||||
|
|
|
@ -27,41 +27,14 @@ export const _getInitiativeFormula = function(combatant) {
|
|||
return parts.filter(p => p !== null).join(" + ");
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* TODO: A temporary shim until 0.7.x becomes stable
|
||||
* @override
|
||||
* When the Combat encounter updates - re-render open Actor sheets for combatants in the encounter.
|
||||
*/
|
||||
TokenConfig.getTrackedAttributes = function(data, _path=[]) {
|
||||
|
||||
// Track the path and record found attributes
|
||||
const attributes = {
|
||||
"bar": [],
|
||||
"value": []
|
||||
};
|
||||
|
||||
// Recursively explore the object
|
||||
for ( let [k, v] of Object.entries(data) ) {
|
||||
let p = _path.concat([k]);
|
||||
|
||||
// Check objects for both a "value" and a "max"
|
||||
if ( v instanceof Object ) {
|
||||
const isBar = ("value" in v) && ("max" in v);
|
||||
if ( isBar ) attributes.bar.push(p);
|
||||
else {
|
||||
const inner = this.getTrackedAttributes(data[k], p);
|
||||
attributes.bar.push(...inner.bar);
|
||||
attributes.value.push(...inner.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise identify values which are numeric or null
|
||||
else if ( Number.isNumeric(v) || (v === null) ) {
|
||||
attributes.value.push(p);
|
||||
}
|
||||
Hooks.on("updateCombat", (combat, data, options, userId) => {
|
||||
const updateTurn = ("turn" in data) || ("round" in data);
|
||||
if ( !updateTurn ) return;
|
||||
for ( let t of combat.turns ) {
|
||||
const a = t.actor;
|
||||
if ( t.actor ) t.actor.sheet.render(false);
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
});
|
||||
|
|
|
@ -325,17 +325,31 @@ SW5E.armorPropertiesTypes = {
|
|||
"Versatile": "SW5E.ArmorProperVersatile"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/**
|
||||
* The valid units of measure for movement distances in the game system.
|
||||
* By default this uses the imperial units of feet and miles.
|
||||
* @type {Object<string,string>}
|
||||
*/
|
||||
SW5E.movementUnits = {
|
||||
"ft": "SW5E.DistFt",
|
||||
"mi": "SW5E.DistMi"
|
||||
}
|
||||
|
||||
/**
|
||||
* The valid units of measure for the range of an action or effect.
|
||||
* This object automatically includes the movement units from SW5E.movementUnits
|
||||
* @type {Object<string,string>}
|
||||
*/
|
||||
SW5E.distanceUnits = {
|
||||
"none": "SW5E.None",
|
||||
"self": "SW5E.DistSelf",
|
||||
"touch": "SW5E.DistTouch",
|
||||
"ft": "SW5E.DistFt",
|
||||
"mi": "SW5E.DistMi",
|
||||
"spec": "SW5E.Special",
|
||||
"any": "SW5E.DistAny"
|
||||
};
|
||||
for ( let [k, v] of Object.entries(SW5E.movementUnits) ) {
|
||||
SW5E.distanceUnits[k] = v;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
@ -413,7 +427,7 @@ SW5E.healingTypes = {
|
|||
* Enumerate the denominations of hit dice which can apply to classes in the SW5E system
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12"];
|
||||
SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"];
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -461,15 +475,14 @@ SW5E.skills = {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.powerPreparationModes = {
|
||||
"prepared": "SW5E.PowerPrepPrepared",
|
||||
"always": "SW5E.PowerPrepAlways",
|
||||
"atwill": "SW5E.PowerPrepAtWill",
|
||||
"innate": "SW5E.PowerPrepInnate",
|
||||
"prepared": "SW5E.PowerPrepPrepared"
|
||||
"innate": "SW5E.PowerPrepInnate"
|
||||
};
|
||||
|
||||
SW5E.powerUpcastModes = ["always", "pact", "prepared"];
|
||||
|
||||
|
||||
SW5E.powerProgression = {
|
||||
"none": "SW5E.PowerNone",
|
||||
"full": "SW5E.PowerProgFull",
|
||||
|
@ -1135,15 +1148,27 @@ SW5E.characterFlags = {
|
|||
type: Boolean
|
||||
},
|
||||
"weaponCriticalThreshold": {
|
||||
name: "SW5E.FlagsCritThreshold",
|
||||
hint: "SW5E.FlagsCritThresholdHint",
|
||||
name: "SW5E.FlagsWeaponCritThreshold",
|
||||
hint: "SW5E.FlagsWeaponCritThresholdHint",
|
||||
section: "Feats",
|
||||
type: Number,
|
||||
placeholder: 20
|
||||
},
|
||||
"powerCriticalThreshold": {
|
||||
name: "SW5E.FlagsPowerCritThreshold",
|
||||
hint: "SW5E.FlagsPowerCritThresholdHint",
|
||||
section: "Feats",
|
||||
type: Number,
|
||||
placeholder: 20
|
||||
},
|
||||
"meleeCriticalDamageDice": {
|
||||
name: "SW5E.FlagsMeleeCriticalDice",
|
||||
hint: "SW5E.FlagsMeleeCriticalDiceHint",
|
||||
section: "Feats",
|
||||
type: Number,
|
||||
placeholder: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Configure allowed status flags
|
||||
SW5E.allowedActorFlags = [
|
||||
"isPolymorphed", "originalActor"
|
||||
].concat(Object.keys(SW5E.characterFlags));
|
||||
SW5E.allowedActorFlags = ["isPolymorphed", "originalActor"].concat(Object.keys(SW5E.characterFlags));
|
||||
|
|
273
module/dice.js
273
module/dice.js
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
|
||||
*
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
|
||||
*
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object} event The triggering event which initiated the roll
|
||||
|
@ -27,72 +27,72 @@
|
|||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
|
||||
chatMessage=true, messageData={}}={}) {
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
|
||||
// Handle fast-forward events
|
||||
let adv = 0;
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (fastForward) {
|
||||
if ( advantage || event.altKey ) adv = 1;
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
|
||||
*/
|
||||
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
|
||||
chatMessage=true, messageData={}}={}) {
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
|
||||
// Handle fast-forward events
|
||||
let adv = 0;
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (fastForward) {
|
||||
if ( advantage || event.altKey ) adv = 1;
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
|
||||
}
|
||||
|
||||
// Define the inner roll function
|
||||
const _roll = (parts, adv, form) => {
|
||||
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if (adv === 1) {
|
||||
nd = elvenAccuracy ? 3 : 2;
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
// Define the inner roll function
|
||||
const _roll = (parts, adv, form) => {
|
||||
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if (adv === 1) {
|
||||
nd = elvenAccuracy ? 3 : 2;
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
// Handle disadvantage
|
||||
else if (adv === -1) {
|
||||
nd = 2;
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
|
||||
mods += "kl";
|
||||
}
|
||||
// Handle disadvantage
|
||||
else if (adv === -1) {
|
||||
nd = 2;
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
|
||||
mods += "kl";
|
||||
}
|
||||
|
||||
// Prepend the d20 roll
|
||||
let formula = `${nd}d20${mods}`;
|
||||
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
|
||||
parts.unshift(formula);
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form ) {
|
||||
data['bonus'] = form.bonus.value;
|
||||
messageOptions.rollMode = form.rollMode.value;
|
||||
}
|
||||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Optionally include an ability score selection (used for tool checks)
|
||||
const ability = form ? form.ability : null;
|
||||
if (ability && ability.value) {
|
||||
data.ability = ability.value;
|
||||
const abl = data.abilities[data.ability];
|
||||
if (abl) {
|
||||
data.mod = abl.mod;
|
||||
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
// Optionally include a situational bonus
|
||||
if ( form ) {
|
||||
data['bonus'] = form.bonus.value;
|
||||
messageOptions.rollMode = form.rollMode.value;
|
||||
}
|
||||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Optionally include an ability score selection (used for tool checks)
|
||||
const ability = form ? form.ability : null;
|
||||
if (ability && ability.value) {
|
||||
data.ability = ability.value;
|
||||
const abl = data.abilities[data.ability];
|
||||
if (abl) {
|
||||
data.mod = abl.mod;
|
||||
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the roll
|
||||
let roll = new Roll(parts.join(" + "), data);
|
||||
|
@ -139,73 +139,76 @@
|
|||
*/
|
||||
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.Dice.rollModes,
|
||||
config: CONFIG.SW5E
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.Dice.rollModes,
|
||||
config: CONFIG.SW5E
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
advantage: {
|
||||
label: game.i18n.localize("SW5E.Advantage"),
|
||||
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize("SW5E.Normal"),
|
||||
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
|
||||
},
|
||||
disadvantage: {
|
||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
|
||||
}
|
||||
// Create the Dialog window
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
advantage: {
|
||||
label: game.i18n.localize("SW5E.Advantage"),
|
||||
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
|
||||
},
|
||||
default: "normal",
|
||||
close: () => resolve(null)
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
}
|
||||
normal: {
|
||||
label: game.i18n.localize("SW5E.Normal"),
|
||||
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
|
||||
},
|
||||
disadvantage: {
|
||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
|
||||
}
|
||||
},
|
||||
default: "normal",
|
||||
close: () => resolve(null)
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
|
||||
*
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Actor} actor The Actor making the damage roll
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object}[event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {String} template The HTML template used to render the roll dialog
|
||||
* @param {String} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string} flavor Flavor text to use in the posted chat message
|
||||
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
|
||||
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
|
||||
* @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits
|
||||
* @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null,
|
||||
dialogOptions={}, chatMessage=true, messageData={}}={}) {
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
|
||||
*
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Actor} actor The Actor making the damage roll
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object}[event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {String} template The HTML template used to render the roll dialog
|
||||
* @param {String} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string} flavor Flavor text to use in the posted chat message
|
||||
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
|
||||
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, fastForward=null, dialogOptions, chatMessage=true, messageData={}}={}) {
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
|
@ -213,8 +216,8 @@ async function _d20RollDialog({template, title, parts, data, rollMode, dialogOpt
|
|||
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) {
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, crit, form) {
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form ) {
|
||||
|
@ -224,17 +227,17 @@ async function _d20RollDialog({template, title, parts, data, rollMode, dialogOpt
|
|||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Create the damage roll
|
||||
let roll = new Roll(parts.join("+"), data);
|
||||
let roll = new Roll(parts.join("+"), data);
|
||||
|
||||
// Modify the damage formula for critical hits
|
||||
if ( crit === true ) {
|
||||
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
|
||||
let mult = 2;
|
||||
// TODO Backwards compatibility - REMOVE LATER
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) roll.alter(mult, add);
|
||||
else roll.alter(add, mult);
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
|
||||
roll.alter(criticalMultiplier, 0); // Multiply all dice
|
||||
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
|
||||
roll.terms[0].alter(1, criticalBonusDice);
|
||||
roll._formula = roll.formula;
|
||||
}
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
|
||||
}
|
||||
|
||||
// Execute the roll
|
||||
|
|
63
module/effects.js
vendored
Normal file
63
module/effects.js
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
|
||||
* @param {MouseEvent} event The left-click event on the effect control
|
||||
* @param {Actor|Item} owner The owning entity which manages this effect
|
||||
*/
|
||||
export function onManageActiveEffect(event, owner) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const li = a.closest("li");
|
||||
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
|
||||
switch ( a.dataset.action ) {
|
||||
case "create":
|
||||
return ActiveEffect.create({
|
||||
label: "New Effect",
|
||||
icon: "icons/svg/aura.svg",
|
||||
origin: owner.uuid,
|
||||
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
|
||||
disabled: li.dataset.effectType === "inactive"
|
||||
}, owner).create();
|
||||
case "edit":
|
||||
return effect.sheet.render(true);
|
||||
case "delete":
|
||||
return effect.delete();
|
||||
case "toggle":
|
||||
return effect.update({disabled: !effect.data.disabled});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data structure for Active Effects which are currently applied to an Actor or Item.
|
||||
* @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for
|
||||
* @return {object} Data for rendering
|
||||
*/
|
||||
export function prepareActiveEffectCategories(effects) {
|
||||
|
||||
// Define effect header categories
|
||||
const categories = {
|
||||
temporary: {
|
||||
type: "temporary",
|
||||
label: "Temporary Effects",
|
||||
effects: []
|
||||
},
|
||||
passive: {
|
||||
type: "passive",
|
||||
label: "Passive Effects",
|
||||
effects: []
|
||||
},
|
||||
inactive: {
|
||||
type: "inactive",
|
||||
label: "Inactive Effects",
|
||||
effects: []
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over active effects, classifying them into categories
|
||||
for ( let e of effects ) {
|
||||
e._getSourceName(); // Trigger a lookup for the source name
|
||||
if ( e.data.disabled ) categories.inactive.effects.push(e);
|
||||
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
||||
else categories.passive.effects.push(e);
|
||||
}
|
||||
return categories;
|
||||
}
|
|
@ -161,6 +161,7 @@ export default class Item5e extends Item {
|
|||
|
||||
// Power Level, School, and Components
|
||||
if ( itemData.type === "power" ) {
|
||||
data.preparation.mode = data.preparation.mode || "prepared";
|
||||
labels.level = C.powerLevels[data.level];
|
||||
labels.school = C.powerSchools[data.school];
|
||||
labels.components = Object.entries(data.components).reduce((arr, c) => {
|
||||
|
@ -180,33 +181,33 @@ export default class Item5e extends Item {
|
|||
else labels.featType = game.i18n.localize("SW5E.Passive");
|
||||
}
|
||||
|
||||
// Species Items
|
||||
else if ( itemData.type === "species" ) {
|
||||
// labels.species = C.species[data.species];
|
||||
}
|
||||
// Archetype Items
|
||||
else if ( itemData.type === "archetype" ) {
|
||||
// Species Items
|
||||
else if ( itemData.type === "species" ) {
|
||||
// labels.species = C.species[data.species];
|
||||
}
|
||||
// Archetype Items
|
||||
else if ( itemData.type === "archetype" ) {
|
||||
// labels.archetype = C.archetype[data.archetype];
|
||||
}
|
||||
// Background Items
|
||||
else if ( itemData.type === "background" ) {
|
||||
|
||||
}
|
||||
// Class Feature Items
|
||||
// Background Items
|
||||
else if ( itemData.type === "background" ) {
|
||||
// labels.background = C.background[data.background];
|
||||
}
|
||||
// Class Feature Items
|
||||
else if ( itemData.type === "classfeature" ) {
|
||||
|
||||
// labels.classFeature = C.classFeature[data.classFeature];
|
||||
}
|
||||
// Fighting Style Items
|
||||
else if ( itemData.type === "fightingstyle" ) {
|
||||
|
||||
// labels.fightingstyle = C.fightingstyle[data.fightingstyle];
|
||||
}
|
||||
// Fighting Mastery Items
|
||||
else if ( itemData.type === "fightingmastery" ) {
|
||||
|
||||
// labels.fightingmastery = C.fightingmastery[data.fightingmastery];
|
||||
}
|
||||
// Lightsaber Form Items
|
||||
else if ( itemData.type === "lightsaberform" ) {
|
||||
|
||||
// labels.lightsaberform = C.lightsaberform[data.lightsaberform];
|
||||
}
|
||||
|
||||
// Equipment Items
|
||||
|
@ -322,7 +323,7 @@ export default class Item5e extends Item {
|
|||
user: game.user._id,
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
|
||||
content: html,
|
||||
flavor: this.name,
|
||||
flavor: this.data.data.chatFlavor || this.name,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
token: this.actor.token,
|
||||
|
@ -348,7 +349,7 @@ export default class Item5e extends Item {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
/**
|
||||
* 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)
|
||||
|
@ -367,7 +368,6 @@ export default class Item5e extends Item {
|
|||
if ( !consume.type ) return true;
|
||||
const actor = this.actor;
|
||||
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
|
||||
const amount = parseInt(consume.amount || 1);
|
||||
|
||||
// Only handle certain types for certain actions
|
||||
if ( ((consume.type === "ammo") && !isAttack ) || ((consume.type !== "ammo") && !isCard) ) return true;
|
||||
|
@ -380,6 +380,7 @@ export default class Item5e extends Item {
|
|||
|
||||
// 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":
|
||||
|
@ -393,7 +394,13 @@ export default class Item5e extends Item {
|
|||
break;
|
||||
case "charges":
|
||||
consumed = actor.items.get(consume.target);
|
||||
quantity = consumed ? consumed.data.data.uses.value : 0;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -418,7 +425,11 @@ export default class Item5e extends Item {
|
|||
await consumed.update({"data.quantity": remaining});
|
||||
break;
|
||||
case "charges":
|
||||
await consumed.update({"data.uses.value": remaining});
|
||||
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;
|
||||
}
|
||||
|
@ -436,7 +447,7 @@ export default class Item5e extends Item {
|
|||
// 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 > 0);
|
||||
let usesCharges = !!uses.per && !!uses.max;
|
||||
let placeTemplate = false;
|
||||
let consume = charge.value || usesCharges;
|
||||
|
||||
|
@ -637,40 +648,37 @@ export default class Item5e extends Item {
|
|||
}
|
||||
|
||||
// Attack Bonus
|
||||
if ( itemData.attackBonus ) parts.push(itemData.attackBonus);
|
||||
const actorBonus = actorData?.bonuses?.[itemData.actionType] || {};
|
||||
if ( itemData.attackBonus || actorBonus.attack ) {
|
||||
parts.push("@atk");
|
||||
rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + ");
|
||||
}
|
||||
if ( actorBonus.attack ) parts.push(actorBonus.attack);
|
||||
|
||||
// Ammunition Bonus
|
||||
delete this._ammo;
|
||||
const consume = itemData.consume;
|
||||
if ( consume?.type === "ammo" ) {
|
||||
const ammo = this.actor.items.get(consume.target);
|
||||
if(ammo?.data){
|
||||
const q = ammo.data.data.quantity;
|
||||
const consumeAmount = consume.amount ?? 0;
|
||||
if ( q && (q - consumeAmount >= 0) ) {
|
||||
let ammoBonus = ammo.data.data.attackBonus;
|
||||
if ( ammoBonus ) {
|
||||
parts.push("@ammo");
|
||||
rollData["ammo"] = ammoBonus;
|
||||
title += ` [${ammo.name}]`;
|
||||
this._ammo = ammo;
|
||||
}
|
||||
// Ammunition Bonus
|
||||
delete this._ammo;
|
||||
const consume = itemData.consume;
|
||||
if ( consume?.type === "ammo" ) {
|
||||
const ammo = this.actor.items.get(consume.target);
|
||||
if(ammo?.data){
|
||||
const q = ammo.data.data.quantity;
|
||||
const consumeAmount = consume.amount ?? 0;
|
||||
if ( q && (q - consumeAmount >= 0) ) {
|
||||
this._ammo = ammo;
|
||||
let ammoBonus = ammo.data.data.attackBonus;
|
||||
if ( ammoBonus ) {
|
||||
parts.push("@ammo");
|
||||
rollData["ammo"] = ammoBonus;
|
||||
title += ` [${ammo.name}]`;
|
||||
}
|
||||
//}else{
|
||||
// ui.notifications.error(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Compose roll options
|
||||
const rollConfig = mergeObject({
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
data: rollData,
|
||||
title: title,
|
||||
flavor: title,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
|
@ -681,9 +689,11 @@ export default class Item5e extends Item {
|
|||
}, options);
|
||||
rollConfig.event = options.event;
|
||||
|
||||
// Expanded weapon critical threshold
|
||||
// Expanded critical hit thresholds
|
||||
if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) {
|
||||
rollConfig.critical = parseInt(flags.weaponCriticalThreshold);
|
||||
} else if (( this.data.type === "power" ) && flags.powerCriticalThreshold) {
|
||||
rollConfig.critical = parseInt(flags.powerCriticalThreshold);
|
||||
}
|
||||
|
||||
// Elven Accuracy
|
||||
|
@ -710,28 +720,41 @@ 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
|
||||
*
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
* Rely upon the damageRoll logic for the core implementation.
|
||||
* @param {MouseEvent} [event] An event which triggered this roll, if any
|
||||
* @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}={}) {
|
||||
rollDamage({event, 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;
|
||||
if ( !this.hasDamage ) {
|
||||
throw new Error("You may not make a Damage Roll with this Item.");
|
||||
}
|
||||
const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id }};
|
||||
|
||||
// Get roll data
|
||||
const parts = itemData.damage.parts.map(d => d[0]);
|
||||
const rollData = this.getRollData();
|
||||
if ( powerLevel ) rollData.item.level = powerLevel;
|
||||
|
||||
// Get message labels
|
||||
// Configure the damage roll
|
||||
const title = `${this.name} - ${game.i18n.localize("SW5E.DamageRoll")}`;
|
||||
let flavor = this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title;
|
||||
|
||||
// Define Roll parts
|
||||
const parts = itemData.damage.parts.map(d => d[0]);
|
||||
const rollConfig = {
|
||||
event: event,
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
data: rollData,
|
||||
title: title,
|
||||
flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
top: event ? event.clientY - 80 : null,
|
||||
left: window.innerWidth - 710
|
||||
},
|
||||
messageData: messageData
|
||||
};
|
||||
|
||||
// Adjust damage from versatile usage
|
||||
if ( versatile && itemData.damage.versatile ) {
|
||||
|
@ -751,37 +774,27 @@ export default class Item5e extends Item {
|
|||
}
|
||||
}
|
||||
|
||||
// Define Roll Data
|
||||
// Add damage bonus formula
|
||||
const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {};
|
||||
if ( actorBonus.damage && parseInt(actorBonus.damage) !== 0 ) {
|
||||
parts.push("@dmg");
|
||||
rollData["dmg"] = actorBonus.damage;
|
||||
if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) {
|
||||
parts.push(actorBonus.damage);
|
||||
}
|
||||
|
||||
// Ammunition Damage
|
||||
// Add ammunition damage
|
||||
if ( this._ammo ) {
|
||||
parts.push("@ammo");
|
||||
rollData["ammo"] = this._ammo.data.data.damage.parts.map(p => p[0]).join("+");
|
||||
flavor += ` [${this._ammo.name}]`;
|
||||
rollConfig.flavor += ` [${this._ammo.name}]`;
|
||||
delete this._ammo;
|
||||
}
|
||||
|
||||
// Scale melee critical hit damage
|
||||
if ( itemData.actionType === "mwak" ) {
|
||||
rollConfig.criticalBonusDice = this.actor.getFlag("sw5e", "meleeCriticalDamageDice") ?? 0;
|
||||
}
|
||||
|
||||
// Call the roll helper utility
|
||||
return damageRoll({
|
||||
event: event,
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
data: rollData,
|
||||
title: title,
|
||||
flavor: flavor,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
top: event ? event.clientY - 80 : null,
|
||||
left: window.innerWidth - 710
|
||||
},
|
||||
messageData
|
||||
});
|
||||
return damageRoll(mergeObject(rollConfig, options));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -793,22 +806,7 @@ export default class Item5e extends Item {
|
|||
_scaleAtWillDamage(parts, scale, level, rollData) {
|
||||
const add = Math.floor((level + 1) / 6);
|
||||
if ( add === 0 ) return;
|
||||
|
||||
// FUTURE SOLUTION - 0.7.0 AND LATER
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) {
|
||||
this._scaleDamage(parts, scale || parts.join(" + "), add, rollData)
|
||||
|
||||
}
|
||||
|
||||
// LEGACY SOLUTION - 0.6.x AND OLDER
|
||||
// TODO: Deprecate the legacy solution one FVTT 0.7.x is RELEASE
|
||||
else {
|
||||
if ( scale && (scale !== parts[0]) ) {
|
||||
parts[0] = parts[0] + " + " + scale.replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${add}d${d}`);
|
||||
} else {
|
||||
parts[0] = parts[0].replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${parseInt(nd)+add}d${d}`);
|
||||
}
|
||||
}
|
||||
this._scaleDamage(parts, scale || parts.join(" + "), add, rollData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -826,20 +824,7 @@ export default class Item5e extends Item {
|
|||
_scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) {
|
||||
const upcastLevels = Math.max(powerLevel - baseLevel, 0);
|
||||
if ( upcastLevels === 0 ) return parts;
|
||||
|
||||
// FUTURE SOLUTION - 0.7.0 AND LATER
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) {
|
||||
this._scaleDamage(parts, formula, upcastLevels, rollData);
|
||||
}
|
||||
|
||||
// LEGACY SOLUTION - 0.6.x AND OLDER
|
||||
// TODO: Deprecate the legacy solution one FVTT 0.7.x is RELEASE
|
||||
else {
|
||||
const bonus = new Roll(formula);
|
||||
bonus.alter(0, upcastLevels);
|
||||
parts.push(bonus.formula);
|
||||
}
|
||||
return parts;
|
||||
this._scaleDamage(parts, formula, upcastLevels, rollData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -882,7 +867,7 @@ export default class Item5e extends Item {
|
|||
/**
|
||||
* Place an attack roll using an item (weapon, feat, power, or equipment)
|
||||
* Rely upon the d20Roll logic for the core implementation
|
||||
*
|
||||
*
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
async rollFormula(options={}) {
|
||||
|
@ -899,7 +884,7 @@ export default class Item5e extends Item {
|
|||
const roll = new Roll(rollData.item.formula, rollData).roll();
|
||||
roll.toMessage({
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
flavor: this.data.data.chatFlavor || title,
|
||||
flavor: title,
|
||||
rollMode: game.settings.get("core", "rollMode"),
|
||||
messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id }}
|
||||
});
|
||||
|
@ -910,7 +895,7 @@ 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
|
||||
* @param {boolean} configureDialog Whether to show a configuration dialog
|
||||
* @return {boolean} Whether further execution should be prevented
|
||||
* @private
|
||||
*/
|
||||
|
@ -972,7 +957,7 @@ export default class Item5e extends Item {
|
|||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
@ -1021,7 +1006,7 @@ export default class Item5e extends Item {
|
|||
template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
|
||||
title: title,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
flavor: `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`,
|
||||
flavor: title,
|
||||
dialogOptions: {
|
||||
width: 400,
|
||||
top: options.event ? options.event.clientY - 80 : null,
|
||||
|
@ -1055,8 +1040,8 @@ export default class Item5e extends Item {
|
|||
}
|
||||
|
||||
// Include a proficiency score
|
||||
const prof = "proficient" in rollData.item ? (rollData.item.proficient || 0) : 1;
|
||||
rollData["prof"] = Math.floor(prof * rollData.attributes.prof);
|
||||
const prof = ("proficient" in rollData.item) ? (rollData.item.proficient || 0) : 1;
|
||||
rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0));
|
||||
return rollData;
|
||||
}
|
||||
|
||||
|
@ -1189,7 +1174,7 @@ export default class Item5e extends Item {
|
|||
if ( !targets.length ) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
|
||||
return targets;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Factory Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import TraitSelector from "../apps/trait-selector.js";
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
|
||||
|
||||
/**
|
||||
* Override and extend the core ItemSheet implementation to handle specific item types
|
||||
|
@ -7,8 +8,11 @@ import TraitSelector from "../apps/trait-selector.js";
|
|||
export default class ItemSheet5e extends ItemSheet {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// Expand the default size of the class sheet
|
||||
if ( this.object.data.type === "class" ) {
|
||||
this.options.width = 600;
|
||||
this.options.width = this.position.width = 600;
|
||||
this.options.height = this.position.height = 680;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +22,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: "auto",
|
||||
height: 400,
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
resizable: true,
|
||||
scrollY: [".tab.details"],
|
||||
|
@ -37,17 +41,17 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
async getData(options) {
|
||||
const data = super.getData(options);
|
||||
data.labels = this.item.labels;
|
||||
data.config = CONFIG.SW5E;
|
||||
|
||||
// Item Type, Status, and Details
|
||||
data.itemType = data.item.type.titleCase();
|
||||
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
|
||||
data.itemStatus = this._getItemStatus(data.item);
|
||||
data.itemProperties = this._getItemProperties(data.item);
|
||||
data.isPhysical = data.item.data.hasOwnProperty("quantity");
|
||||
|
||||
|
||||
// Potential consumption targets
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
|
||||
|
||||
|
@ -55,17 +59,20 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
data.hasAttackRoll = this.item.hasAttack;
|
||||
data.isHealing = data.item.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
|
||||
// Vehicles
|
||||
data.isCrewed = data.item.data.activation?.type === 'crew';
|
||||
data.isMountable = this._isItemMountable(data.item);
|
||||
|
||||
// Prepare Active Effects
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
/**
|
||||
* Get the valid item consumption targets which exist on the actor
|
||||
* @param {Object} item Item data for the item being displayed
|
||||
* @return {{string: string}} An object of potential consumption targets
|
||||
|
@ -109,6 +116,8 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
// Charges
|
||||
else if ( consume.type === "charges" ) {
|
||||
return actor.items.reduce((obj, i) => {
|
||||
|
||||
// Limited-use items
|
||||
const uses = i.data.data.uses || {};
|
||||
if ( uses.per && uses.max ) {
|
||||
const label = uses.per === "charges" ?
|
||||
|
@ -116,6 +125,10 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`;
|
||||
obj[i.id] = i.name + label;
|
||||
}
|
||||
|
||||
// Recharging items
|
||||
const recharge = i.data.data.recharge || {};
|
||||
if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
||||
return obj;
|
||||
}, {})
|
||||
}
|
||||
|
@ -177,31 +190,26 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
}
|
||||
|
||||
else if ( item.type === "species" ) {
|
||||
|
||||
//props.push(labels.species);
|
||||
}
|
||||
else if ( item.type === "archetype" ) {
|
||||
|
||||
//props.push(labels.archetype);
|
||||
}
|
||||
else if ( item.type === "background" ) {
|
||||
//props.push(labels.background);
|
||||
}
|
||||
else if ( item.type === "classfeature" ) {
|
||||
//props.push(labels.classfeature);
|
||||
}
|
||||
else if ( item.type === "fightingmastery" ) {
|
||||
//props.push(labels.fightingmastery);
|
||||
}
|
||||
else if ( item.type === "fightingstyle" ) {
|
||||
//props.push(labels.fightingstyle);
|
||||
}
|
||||
else if ( item.type === "lightsaberform" ) {
|
||||
//props.push(labels.lightsaberform);
|
||||
}
|
||||
|
||||
else if ( item.type === "background" ) {
|
||||
|
||||
}
|
||||
|
||||
else if ( item.type === "classfeature" ) {
|
||||
|
||||
}
|
||||
|
||||
else if ( item.type === "fightingmastery" ) {
|
||||
|
||||
}
|
||||
|
||||
else if ( item.type === "fightingstyle" ) {
|
||||
|
||||
}
|
||||
|
||||
else if ( item.type === "lightsaberform" ) {
|
||||
|
||||
}
|
||||
|
||||
// Action type
|
||||
if ( item.data.actionType ) {
|
||||
|
@ -240,8 +248,8 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/** @override */
|
||||
setPosition(position={}) {
|
||||
if ( !this._minimized ) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
if ( !(this._minimized || position.height) ) {
|
||||
position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height;
|
||||
}
|
||||
return super.setPosition(position);
|
||||
}
|
||||
|
@ -251,17 +259,20 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
_getSubmitData(updateData={}) {
|
||||
|
||||
// TODO: This can be removed once 0.7.x is release channel
|
||||
if ( !formData.data ) formData = expandObject(formData);
|
||||
// Create the expanded update data object
|
||||
const fd = new FormDataExtended(this.form, {editors: this.editors});
|
||||
let data = fd.toObject();
|
||||
if ( updateData ) data = mergeObject(data, updateData);
|
||||
else data = expandObject(data);
|
||||
|
||||
// Handle Damage Array
|
||||
const damage = formData.data?.damage;
|
||||
// Handle Damage array
|
||||
const damage = data.data?.damage;
|
||||
if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
|
||||
|
||||
// Update the Item
|
||||
super._updateObject(event, formData);
|
||||
// Return the flattened submission data
|
||||
return flattenObject(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -269,8 +280,14 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
|
||||
if ( this.isEditable ) {
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
|
||||
html.find(".effect-control").click(ev => {
|
||||
if ( this.item.isOwned ) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.")
|
||||
onManageActiveEffect(ev, this.item)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -328,4 +345,12 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
maximum: skills.number
|
||||
}).render(true)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onSubmit(...args) {
|
||||
if ( this._tabs[0].active === "details" ) this.position.height = "auto";
|
||||
await super._onSubmit(...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export const migrateWorld = async function() {
|
|||
await a.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +28,7 @@ export const migrateWorld = async function() {
|
|||
await i.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
@ -40,15 +42,15 @@ export const migrateWorld = async function() {
|
|||
await s.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Compendium Packs
|
||||
const packs = game.packs.filter(p => {
|
||||
return (p.metadata.package === "world") && ["Actor", "Item", "Scene"].includes(p.metadata.entity)
|
||||
});
|
||||
for ( let p of packs ) {
|
||||
for ( let p of game.packs ) {
|
||||
if ( p.metadata.package !== "world" ) continue;
|
||||
if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue;
|
||||
await migrateCompendium(p);
|
||||
}
|
||||
|
||||
|
@ -68,27 +70,46 @@ export const migrateCompendium = async function(pack) {
|
|||
const entity = pack.metadata.entity;
|
||||
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
|
||||
|
||||
// Unlock the pack for editing
|
||||
const wasLocked = pack.locked;
|
||||
await pack.configure({locked: false});
|
||||
|
||||
// Begin by requesting server-side data model migration and get the migrated content
|
||||
await pack.migrate();
|
||||
const content = await pack.getContent();
|
||||
|
||||
// Iterate over compendium entries - applying fine-tuned migration functions
|
||||
for ( let ent of content ) {
|
||||
let updateData = {};
|
||||
try {
|
||||
let updateData = null;
|
||||
if (entity === "Item") updateData = migrateItemData(ent.data);
|
||||
else if (entity === "Actor") updateData = migrateActorData(ent.data);
|
||||
else if ( entity === "Scene" ) updateData = migrateSceneData(ent.data);
|
||||
if (!isObjectEmpty(updateData)) {
|
||||
expandObject(updateData);
|
||||
updateData["_id"] = ent._id;
|
||||
await pack.updateEntity(updateData);
|
||||
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
|
||||
switch (entity) {
|
||||
case "Actor":
|
||||
updateData = migrateActorData(ent.data);
|
||||
break;
|
||||
case "Item":
|
||||
updateData = migrateItemData(ent.data);
|
||||
break;
|
||||
case "Scene":
|
||||
updateData = migrateSceneData(ent.data);
|
||||
break;
|
||||
}
|
||||
} catch(err) {
|
||||
if ( isObjectEmpty(updateData) ) continue;
|
||||
|
||||
// Save the entry, if data was changed
|
||||
updateData["_id"] = ent._id;
|
||||
await pack.updateEntity(updateData);
|
||||
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
|
||||
}
|
||||
|
||||
// Handle migration failures
|
||||
catch(err) {
|
||||
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the original locked status for the pack
|
||||
pack.configure({locked: wasLocked});
|
||||
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
|
||||
};
|
||||
|
||||
|
@ -107,9 +128,7 @@ export const migrateActorData = function(actor) {
|
|||
|
||||
// Actor Data Updates
|
||||
_migrateActorBonuses(actor, updateData);
|
||||
|
||||
// Remove deprecated fields
|
||||
_migrateRemoveDeprecated(actor, updateData);
|
||||
_migrateActorMovement(actor, updateData);
|
||||
|
||||
// Migrate Owned Items
|
||||
if ( !actor.items ) return updateData;
|
||||
|
@ -172,11 +191,6 @@ function cleanActorData(actorData) {
|
|||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
|
||||
// Remove deprecated fields
|
||||
_migrateRemoveDeprecated(item, updateData);
|
||||
|
||||
// Return the migrated update data
|
||||
return updateData;
|
||||
};
|
||||
|
||||
|
@ -225,31 +239,17 @@ function _migrateActorBonuses(actor, updateData) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* A general migration to remove all fields from the data model which are flagged with a _deprecated tag
|
||||
* Migrate the actor bonuses object
|
||||
* @private
|
||||
*/
|
||||
const _migrateRemoveDeprecated = function(ent, updateData) {
|
||||
const flat = flattenObject(ent.data);
|
||||
|
||||
// Identify objects to deprecate
|
||||
const toDeprecate = Object.entries(flat).filter(e => e[0].endsWith("_deprecated") && (e[1] === true)).map(e => {
|
||||
let parent = e[0].split(".");
|
||||
parent.pop();
|
||||
return parent.join(".");
|
||||
});
|
||||
|
||||
// Remove them
|
||||
for ( let k of toDeprecate ) {
|
||||
let parts = k.split(".");
|
||||
parts[parts.length-1] = "-=" + parts[parts.length-1];
|
||||
updateData[`data.${parts.join(".")}`] = null;
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -280,3 +280,24 @@ export async function purgeFlags(pack) {
|
|||
}
|
||||
await pack.configure({locked: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Purge the data model of any inner objects which have been flagged as _deprecated.
|
||||
* @param {object} data The data to clean
|
||||
* @private
|
||||
*/
|
||||
export function removeDeprecatedObjects(data) {
|
||||
for ( let [k, v] of Object.entries(data) ) {
|
||||
if ( getType(v) === "Object" ) {
|
||||
if (v._deprecated === true) {
|
||||
console.log(`Deleting deprecated object key ${k}`);
|
||||
delete data[k];
|
||||
}
|
||||
else removeDeprecatedObjects(v);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
templateData.width = target.value;
|
||||
templateData.direction = 45;
|
||||
break;
|
||||
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
|
||||
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
|
||||
templateData.width = target.width ?? canvas.dimensions.distance;
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -7,11 +7,11 @@ export const registerSystemSettings = function() {
|
|||
name: "System Migration Version",
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: Number,
|
||||
default: 0
|
||||
type: String,
|
||||
default: ""
|
||||
});
|
||||
|
||||
/**
|
||||
/**
|
||||
* Register resting variants
|
||||
*/
|
||||
game.settings.register("sw5e", "restVariant", {
|
||||
|
@ -82,18 +82,6 @@ export const registerSystemSettings = function() {
|
|||
type: Boolean,
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to automatically create Power Measured Template on roll
|
||||
*/
|
||||
game.settings.register("sw5e", "alwaysPlacePowerTemplate", {
|
||||
name: "SETTINGS.5eAutoPowerTemplateN",
|
||||
hint: "SETTINGS.5eAutoPowerTemplateL",
|
||||
scope: "client",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to automatically collapse Item Card descriptions
|
||||
*/
|
||||
|
@ -142,4 +130,16 @@ export const registerSystemSettings = function() {
|
|||
transformTokens: true
|
||||
}
|
||||
});
|
||||
game.settings.register("sw5e", "colorTheme", {
|
||||
name: "SETTINGS.SWColorN",
|
||||
hint: "SETTINGS.SWColorL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: "light",
|
||||
type: String,
|
||||
choices: {
|
||||
"light": "SETTINGS.SWColorLight",
|
||||
"dark": "SETTINGS.SWColorDark"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,24 +4,30 @@
|
|||
* @return {Promise}
|
||||
*/
|
||||
export const preloadHandlebarsTemplates = async function() {
|
||||
return loadTemplates([
|
||||
|
||||
// Define template paths to load
|
||||
const templatePaths = [
|
||||
// Shared Partials
|
||||
"systems/sw5e/templates/actors/parts/active-effects.html",
|
||||
|
||||
// Actor Sheet Partials
|
||||
"systems/sw5e/templates/actors/parts/actor-traits.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-inventory.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-features.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-powerbook.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-effects.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-traits.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-features.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html",
|
||||
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-notes.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-powerbook.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",
|
||||
|
||||
// Item Sheet Partials
|
||||
"systems/sw5e/templates/items/parts/item-action.html",
|
||||
"systems/sw5e/templates/items/parts/item-activation.html",
|
||||
"systems/sw5e/templates/items/parts/item-description.html",
|
||||
"systems/sw5e/templates/items/parts/item-mountable.html"
|
||||
];
|
||||
|
||||
// Load the template parts
|
||||
return loadTemplates(templatePaths);
|
||||
]);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue