forked from GitHub-Mirrors/foundry-sw5e
Updating to Push Class skills on level up
Update from DND5E beta for Class Skills on level up and other various fixups
This commit is contained in:
parent
67ba5b2d2d
commit
53d7284596
36 changed files with 1537 additions and 440 deletions
|
@ -64,13 +64,6 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/** @override */
|
||||
prepareBaseData() {
|
||||
|
||||
// Compute initial ability score modifiers in base data since these may be referenced
|
||||
for (let abl of Object.values(this.data.data.abilities)) {
|
||||
abl.mod = Math.floor((abl.value - 10) / 2);
|
||||
}
|
||||
|
||||
// Type-specific base data preparation
|
||||
switch ( this.data.type ) {
|
||||
case "character":
|
||||
return this._prepareCharacterData(this.data);
|
||||
|
@ -107,6 +100,7 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
|
||||
// Ability modifiers and saves
|
||||
const dcBonus = Number.isNumeric(data.bonuses.power.dc) ? parseInt(data.bonuses.power.dc) : 0;
|
||||
const saveBonus = Number.isNumeric(bonuses.save) ? parseInt(bonuses.save) : 0;
|
||||
const checkBonus = Number.isNumeric(bonuses.check) ? parseInt(bonuses.check) : 0;
|
||||
for (let [id, abl] of Object.entries(data.abilities)) {
|
||||
|
@ -115,6 +109,7 @@ export default class Actor5e extends Actor {
|
|||
abl.saveBonus = saveBonus;
|
||||
abl.checkBonus = checkBonus;
|
||||
abl.save = abl.mod + abl.prof + abl.saveBonus;
|
||||
abl.dc = 8 + abl.mod + abl.prof + dcBonus;
|
||||
|
||||
// If we merged saves when transforming, take the highest bonus here.
|
||||
if (originalSaves && abl.proficient) {
|
||||
|
@ -135,7 +130,7 @@ export default class Actor5e extends Actor {
|
|||
init.total = init.mod + init.prof + init.bonus;
|
||||
|
||||
// Prepare power-casting data
|
||||
data.attributes.powerdc = this.getPowerDC(data.attributes.powercasting);
|
||||
this._computePowercastingDC(this.data);
|
||||
this._computePowercastingProgression(this.data);
|
||||
}
|
||||
|
||||
|
@ -165,22 +160,6 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the power DC for this actor using a certain ability score
|
||||
* @param {string} ability The ability score, i.e. "str"
|
||||
* @return {number} The power DC
|
||||
*/
|
||||
getPowerDC(ability) {
|
||||
const actorData = this.data.data;
|
||||
let bonus = getProperty(actorData, "bonuses.power.dc");
|
||||
bonus = Number.isNumeric(bonus) ? parseInt(bonus) : 0;
|
||||
ability = actorData.abilities[ability];
|
||||
const prof = actorData.attributes.prof;
|
||||
return 8 + (ability ? ability.mod : 0) + prof + bonus;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getRollData() {
|
||||
const data = super.getRollData();
|
||||
|
@ -194,6 +173,101 @@ export default class Actor5e extends Actor {
|
|||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return {Promise<Item5e[]>} Array of Item5e entities
|
||||
*/
|
||||
static async getClassFeatures(cls) {
|
||||
const level = cls.data.levels;
|
||||
const className = cls.name.toLowerCase();
|
||||
|
||||
// Get the configuration of features which may be added
|
||||
const clsConfig = CONFIG.SW5E.classFeatures[className];
|
||||
let featureIDs = clsConfig["features"][level] || [];
|
||||
const subclassName = cls.data.subclass.toLowerCase().slugify();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Load item data for all identified features
|
||||
const features = await Promise.all(featureIDs.map(id => fromUuid(id)));
|
||||
|
||||
// Class powers should always be prepared
|
||||
for ( const feature of features ) {
|
||||
if ( feature.type === "power" ) {
|
||||
const preparation = feature.data.data.preparation;
|
||||
preparation.mode = "always";
|
||||
preparation.prepared = true;
|
||||
}
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async updateEmbeddedEntity(embeddedName, data, options={}) {
|
||||
const createItems = embeddedName === "OwnedItem" ? await this._createClassFeatures(data) : [];
|
||||
let updated = await super.updateEmbeddedEntity(embeddedName, data, options);
|
||||
if ( createItems.length ) await this.createEmbeddedEntity("OwnedItem", createItems);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create additional class features in the Actor when a class item is updated.
|
||||
* @private
|
||||
*/
|
||||
async _createClassFeatures(updated) {
|
||||
let toCreate = [];
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
if ( changed ) {
|
||||
const features = await Actor5e.getClassFeatures(classData);
|
||||
if ( features.length ) toCreate.push(...features);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Data Preparation Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
@ -313,6 +387,31 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the powercasting DC for all item abilities which use power DC scaling
|
||||
* @param {object} actorData The actor data being prepared
|
||||
* @private
|
||||
*/
|
||||
_computePowercastingDC(actorData) {
|
||||
|
||||
// Compute the powercasting DC
|
||||
const data = actorData.data;
|
||||
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
|
||||
|
||||
// Apply powercasting DC to any power items which use it
|
||||
for ( let i of this.items ) {
|
||||
const save = i.data.data.save;
|
||||
if ( save?.ability ) {
|
||||
if ( save.scaling === "power" ) save.dc = data.attributes.powerdc;
|
||||
else if ( save.scaling !== "flat" ) save.dc = data.abilities[save.scaling]?.dc ?? 10;
|
||||
const ability = CONFIG.SW5E.abilities[save.ability];
|
||||
i.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare data related to the power-casting capabilities of the Actor
|
||||
* @private
|
||||
|
@ -419,7 +518,7 @@ export default class Actor5e extends Actor {
|
|||
// [Optional] add Currency Weight
|
||||
if ( game.settings.get("sw5e", "currencyWeight") ) {
|
||||
const currency = actorData.data.currency;
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += denom, 0);
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0);
|
||||
weight += Math.round((numCoins * 10) / CONFIG.SW5E.encumbrance.currencyPerWeight) / 10;
|
||||
}
|
||||
|
||||
|
@ -591,12 +690,12 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Update Actor data
|
||||
if ( usesSlots && consumeSlot && (lvl > 0) ) {
|
||||
const slots = parseInt(this.data.data.powers[consumeSlot].value);
|
||||
const slots = parseInt(this.data.data.powers[consumeSlot]?.value);
|
||||
if ( slots === 0 || Number.isNaN(slots) ) {
|
||||
return ui.notifications.error(game.i18n.localize("SW5E.PowerCastNoSlots"));
|
||||
}
|
||||
await this.update({
|
||||
[`data.powers.${consumeSlot}.value`]: Math.max(parseInt(this.data.data.powers[consumeSlot].value) - 1, 0)
|
||||
[`data.powers.${consumeSlot}.value`]: Math.max(slots - 1, 0)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -610,7 +709,7 @@ export default class Actor5e extends Actor {
|
|||
// Initiate ability template placement workflow if selected
|
||||
if ( placeTemplate && item.hasAreaTarget ) {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview(event);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.sheet.rendered ) this.sheet.minimize();
|
||||
}
|
||||
|
||||
|
@ -1004,19 +1103,26 @@ export default class Actor5e extends Actor {
|
|||
await this.updateEmbeddedEntity("OwnedItem", updateItems);
|
||||
|
||||
// Display a Chat Message summarizing the rest effects
|
||||
let restFlavor;
|
||||
switch (game.settings.get("sw5e", "restVariant")) {
|
||||
case 'normal': restFlavor = game.i18n.localize("SW5E.ShortRestNormal"); break;
|
||||
case 'gritty': restFlavor = game.i18n.localize(newDay ? "SW5E.ShortRestOvernight" : "SW5E.ShortRestGritty"); break;
|
||||
case 'epic': restFlavor = game.i18n.localize("SW5E.ShortRestEpic"); break;
|
||||
}
|
||||
|
||||
if ( chat ) {
|
||||
|
||||
// Summarize the rest duration
|
||||
let restFlavor;
|
||||
switch (game.settings.get("sw5e", "restVariant")) {
|
||||
case 'normal': restFlavor = game.i18n.localize("SW5E.ShortRestNormal"); break;
|
||||
case 'gritty': restFlavor = game.i18n.localize(newDay ? "SW5E.ShortRestOvernight" : "SW5E.ShortRestGritty"); break;
|
||||
case 'epic': restFlavor = game.i18n.localize("SW5E.ShortRestEpic"); break;
|
||||
}
|
||||
|
||||
// Summarize the health effects
|
||||
let srMessage = "SW5E.ShortRestResultShort";
|
||||
if ((dhd !== 0) && (dhp !== 0)) srMessage = "SW5E.ShortRestResult";
|
||||
|
||||
// Create a chat message
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
speaker: {actor: this, alias: this.name},
|
||||
flavor: restFlavor,
|
||||
content: game.i18n.format("SW5E.ShortRestResult", {name: this.name, dice: -dhd, health: dhp})
|
||||
content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1036,13 +1142,13 @@ export default class Actor5e extends Actor {
|
|||
* Take a long rest, recovering HP, HD, resources, and power slots
|
||||
* @param {boolean} dialog Present a confirmation dialog window whether or not to take a long rest
|
||||
* @param {boolean} chat Summarize the results of the rest workflow as a chat message
|
||||
* @param {boolean} newDay Whether the long rest carries over to a new day
|
||||
* @return {Promise} A Promise which resolves once the long rest workflow has completed
|
||||
*/
|
||||
async longRest({dialog=true, chat=true}={}) {
|
||||
async longRest({dialog=true, chat=true, newDay=true}={}) {
|
||||
const data = this.data.data;
|
||||
|
||||
// Maybe present a confirmation dialog
|
||||
let newDay = false;
|
||||
if ( dialog ) {
|
||||
try {
|
||||
newDay = await LongRestDialog.longRestDialog({actor: this});
|
||||
|
@ -1120,12 +1226,17 @@ export default class Actor5e extends Actor {
|
|||
case 'epic': restFlavor = game.i18n.localize("SW5E.LongRestEpic"); break;
|
||||
}
|
||||
|
||||
// Determine the chat message to display
|
||||
if ( chat ) {
|
||||
let lrMessage = "SW5E.LongRestResultShort";
|
||||
if((dhp !== 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResult";
|
||||
else if ((dhp !== 0) && (dhd === 0)) lrMessage = "SW5E.LongRestResultHitPoints";
|
||||
else if ((dhp === 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResultHitDice";
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
speaker: {actor: this, alias: this.name},
|
||||
flavor: restFlavor,
|
||||
content: game.i18n.format("SW5E.LongRestResult", {name: this.name, health: dhp, dice: dhd})
|
||||
content: game.i18n.format(lrMessage, {name: this.name, health: dhp, dice: dhd})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1356,4 +1467,16 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* DEPRECATED METHODS */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since sw5e 0.97
|
||||
*/
|
||||
getPowerDC(ability) {
|
||||
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`);
|
||||
return this.data.data.abilities[ability]?.dc;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,18 +163,18 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, level={}) => {
|
||||
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner && (i >= 1),
|
||||
canCreate: owner,
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
powers: [],
|
||||
uses: useLabels[i] || level.value || 0,
|
||||
slots: useLabels[i] || level.max || 0,
|
||||
override: level.override || 0,
|
||||
dataset: {"type": "power", "level": i},
|
||||
uses: useLabels[i] || value || 0,
|
||||
slots: useLabels[i] || max || 0,
|
||||
override: override || 0,
|
||||
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
|
||||
prop: sl
|
||||
};
|
||||
};
|
||||
|
@ -187,7 +187,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
return max;
|
||||
}, 0);
|
||||
|
||||
// Structure the powerbook for every level up to the maximum which has a slot
|
||||
// Level-based powercasters have cantrips and leveled slots
|
||||
if ( maxLevel > 0 ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
for (let lvl = 1; lvl <= maxLevel; lvl++) {
|
||||
|
@ -195,9 +195,18 @@ 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 ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
registerSection("pact", sections.pact, CONFIG.SW5E.powerPreparationModes.pact, levels.pact);
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
const l = levels.pact;
|
||||
const config = CONFIG.SW5E.powerPreparationModes.pact;
|
||||
registerSection("pact", sections.pact, config, {
|
||||
prepMode: "pact",
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
|
||||
// Iterate over every power item, adding powers to the powerbook by section
|
||||
|
@ -206,17 +215,24 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
let s = power.data.level || 0;
|
||||
const sl = `power${s}`;
|
||||
|
||||
// Powercasting mode specific headings
|
||||
// Specialized powercasting modes (if they exist)
|
||||
if ( mode in sections ) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
registerSection(mode, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]);
|
||||
const l = levels[mode] || {};
|
||||
const config = CONFIG.SW5E.powerPreparationModes[mode];
|
||||
registerSection(mode, s, config, {
|
||||
prepMode: mode,
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Higher-level power headings
|
||||
// Sections for higher-level powers which the caster "should not" have, but power items exist for
|
||||
else if ( !powerbook[s] ) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], levels[sl]);
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
|
||||
}
|
||||
|
||||
// Add the power to the relevant heading
|
||||
|
@ -508,13 +524,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
itemData = scroll.data;
|
||||
}
|
||||
|
||||
// Upgrade the number of class levels a character has
|
||||
if ( (itemData.type === "class") && ( this.actor.itemTypes.class.find(c => c.name === itemData.name)) ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const lvl = cls.data.data.levels;
|
||||
return cls.update({"data.levels": Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level)})
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
@ -843,4 +852,4 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
// Return a copy of the extracted data
|
||||
return duplicate(itemData);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../entity.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the SW5E system.
|
||||
|
@ -253,4 +254,32 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
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
|
||||
if ( classWasAlreadyPresent ) {
|
||||
const lvl = cls.data.data.levels;
|
||||
return cls.update({"data.levels": Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level)})
|
||||
}
|
||||
}
|
||||
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,8 +51,8 @@ export default class AbilityUseDialog extends Dialog {
|
|||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
const icon = data.hasPowerSlots ? "fa-magic" : "fa-fist-raised";
|
||||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.hasPowerSlots ? "Cast" : "Use"));
|
||||
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
|
||||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
||||
return new Promise((resolve) => {
|
||||
const dlg = new this(item, {
|
||||
title: `${item.name}: Usage Configuration`,
|
||||
|
@ -85,6 +85,12 @@ export default class AbilityUseDialog extends Dialog {
|
|||
const lvl = itemData.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
|
||||
// If can't upcast, return early and don't bother calculating available power slots
|
||||
if (!canUpcast) {
|
||||
data = mergeObject(data, { isPower: true, canUpcast });
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
|
@ -97,7 +103,7 @@ export default class AbilityUseDialog extends Dialog {
|
|||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
||||
canCast: canUpcast && (max > 0),
|
||||
canCast: max > 0,
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
|
@ -109,14 +115,14 @@ export default class AbilityUseDialog extends Dialog {
|
|||
powerLevels.push({
|
||||
level: 'pact',
|
||||
label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
|
||||
canCast: canUpcast,
|
||||
canCast: true,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
|
||||
// Return merged data
|
||||
data = mergeObject(data, { hasPowerSlots: true, canUpcast, powerLevels });
|
||||
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
|
||||
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
|
||||
}
|
||||
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/**
|
||||
* A specialized Dialog subclass for casting a power item at a certain level
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export class PowerCastDialog 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 power
|
||||
* @type {Actor5e}
|
||||
*/
|
||||
this.actor = actor;
|
||||
|
||||
/**
|
||||
* Store a reference to the Item entity which is the power being cast
|
||||
* @type {Item5e}
|
||||
*/
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A constructor function which displays the Power 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 power may be upcast
|
||||
const lvl = id.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const l = ad.powers["power"+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.powerLevels[i]} (${slots} Slots)` : CONFIG.SW5E.powerLevels[i],
|
||||
canCast: canUpcast && (max > 0),
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
|
||||
const pact = ad.powers.pact;
|
||||
if (pact.level >= lvl) {
|
||||
// If this character has pact slots, present them as an option for
|
||||
// casting the power.
|
||||
powerLevels.push({
|
||||
level: 'pact',
|
||||
label: game.i18n.localize('SW5E.PowerLevelPact')
|
||||
+ ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) `
|
||||
+ `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`,
|
||||
canCast: canUpcast,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
|
||||
// Render the Power casting template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/power-cast.html", {
|
||||
item: item.data,
|
||||
canCast: canCast,
|
||||
canUpcast: canUpcast,
|
||||
powerLevels,
|
||||
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}: Power Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
cast: {
|
||||
icon: '<i class="fas fa-magic"></i>',
|
||||
label: "Cast",
|
||||
callback: html => resolve(new FormData(html[0].querySelector("#power-config-form")))
|
||||
}
|
||||
},
|
||||
default: "cast",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ 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();
|
||||
|
||||
// If the user is the message author or the actor owner, proceed
|
||||
let actor = game.actors.get(data.message.speaker.actor);
|
||||
|
|
694
module/classFeatures.js
Normal file
694
module/classFeatures.js
Normal file
|
@ -0,0 +1,694 @@
|
|||
export const ClassFeatures = {
|
||||
/* These are the class features for DND5E left in for reference to help build SW5E Class features
|
||||
// Remove once SW5E Class features are added.
|
||||
"barbarian": {
|
||||
"subclasses": {
|
||||
"path-of-the-ancestral-guardian": {
|
||||
"label": "Path of the Ancestral Guardian",
|
||||
"source": "XGE pg. 9"
|
||||
},
|
||||
"path-of-the-battlerager": {
|
||||
"label": "Path of the Battlerager",
|
||||
"source": "SCAG pg. 121"
|
||||
},
|
||||
"path-of-the-berserker": {
|
||||
"label": "Path of the Berserker",
|
||||
"source": "PHB pg. 49",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.classfeatures.CkbbAckeCtyHXEnL"],
|
||||
"6": ["Compendium.sw5e.classfeatures.0Jgf8fYY2ExwgQpN"],
|
||||
"10": ["Compendium.sw5e.classfeatures.M6VSMzVtKPhh8B0i"],
|
||||
"14": ["Compendium.sw5e.classfeatures.xzD9zlRP6dUxCtCl"]
|
||||
}
|
||||
},
|
||||
"path-of-the-juggernaut": {
|
||||
"label": "Path of the Juggernaut",
|
||||
"source": "TCS pg. 102"
|
||||
},
|
||||
"path-of-the-storm-herald": {
|
||||
"label": "Path of the Storm Herald",
|
||||
"source": "XGE pg. 10"
|
||||
},
|
||||
"path-of-the-totem-warrior": {
|
||||
"label": "Path of the Totem Warrior",
|
||||
"source": "PHB pg. 50; SCAG pg. 121"
|
||||
},
|
||||
"path-of-the-zealot": {
|
||||
"label": "Path of the Zealot",
|
||||
"source": "XGE pg. 11"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.VoR0SUrNX5EJVPIO", "Compendium.sw5e.classfeatures.SZbsNbaxFFGwBpNK"],
|
||||
"2": ["Compendium.sw5e.classfeatures.SCVjqRdlZ9cvHVSR", "Compendium.sw5e.classfeatures.vt31lWAULygEl7yk"],
|
||||
"3": ["Compendium.sw5e.classfeatures.TH1QAf6YNGSeBVjT"],
|
||||
"5": ["Compendium.sw5e.classfeatures.XogoBnFWmCAHXppo", "Compendium.sw5e.classfeatures.Kl6zifJ5OmdHlOi2"],
|
||||
"7": ["Compendium.sw5e.classfeatures.NlXslw4yAqmKZWtN"],
|
||||
"9": ["Compendium.sw5e.classfeatures.L94gyvNpUhUe0rwh"],
|
||||
"11": ["Compendium.sw5e.classfeatures.FqfmbPgxiyrWzhYk"],
|
||||
"15": ["Compendium.sw5e.classfeatures.l8tUhZ5Pecm9wz7I"],
|
||||
"18": ["Compendium.sw5e.classfeatures.Q1exex5ALteprrPo"],
|
||||
"20": ["Compendium.sw5e.classfeatures.jVU4AgqfrFaqgXns"]
|
||||
}
|
||||
},
|
||||
"bard": {
|
||||
"subclasses": {
|
||||
"college-of-glamour": {
|
||||
"label": "College of Glamour",
|
||||
"source": "XGE pg. 14",
|
||||
"features": {}
|
||||
},
|
||||
"college-of-lore": {
|
||||
"label": "College of Lore",
|
||||
"source": "PHB pg. 54",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.classfeatures.5zPmHPQUne7RDfaU"],
|
||||
"6": ["Compendium.sw5e.classfeatures.myBu3zi5eYvQIcuy"],
|
||||
"14": ["Compendium.sw5e.classfeatures.pquwueEMweRhiWaq"]
|
||||
}
|
||||
},
|
||||
"college-of-swords": {
|
||||
"label": "College of Swords",
|
||||
"source": "XGE pg. 15",
|
||||
"features": {}
|
||||
},
|
||||
"college-of-valor": {
|
||||
"label": "College of Valor",
|
||||
"source": "PHB pg. 55",
|
||||
"features": {}
|
||||
},
|
||||
"college-of-whispers": {
|
||||
"label": "College of Whispers",
|
||||
"source": "XGE pg. 16",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.hpLNiGq7y67d2EHA", "Compendium.sw5e.classfeatures.u4NLajXETJhJU31v"],
|
||||
"2": ["Compendium.sw5e.classfeatures.ezWijmCnlnQ9ZRX2", "Compendium.sw5e.classfeatures.he8RpPXwSl2lVSIk"],
|
||||
"3": ["Compendium.sw5e.classfeatures.ILhzFHiRrqgQ9dFJ", "Compendium.sw5e.classfeatures.aQLg7BWdRnm4Hr9S"],
|
||||
"5": ["Compendium.sw5e.classfeatures.3VDZGs5Ug3hIE322"],
|
||||
"6": ["Compendium.sw5e.classfeatures.SEJmsjkEhdAZ90ki"],
|
||||
"10": ["Compendium.sw5e.classfeatures.aonJ2YjkqkYB9WYB"],
|
||||
"20": ["Compendium.sw5e.classfeatures.GBYN5rH4nh1ocRlY"]
|
||||
}
|
||||
},
|
||||
"cleric": {
|
||||
"subclasses": {
|
||||
"ambition-domain": {
|
||||
"label": "Ambition Domain",
|
||||
"source": "PS:A pg. 27",
|
||||
"features": {}
|
||||
},
|
||||
"arcana-domain": {
|
||||
"label": "Arcana Domain",
|
||||
"source": "SCAG pg. 125",
|
||||
"features": {}
|
||||
},
|
||||
"blood-domain": {
|
||||
"label": "Blood Domain",
|
||||
"source": "TCS pg. 101",
|
||||
"features": {}
|
||||
},
|
||||
"death-domain": {
|
||||
"label": "Death Domain",
|
||||
"source": "DMG pg. 96",
|
||||
"features": {}
|
||||
},
|
||||
"forge-domain": {
|
||||
"label": "Forge Domain",
|
||||
"source": "XGE pg. 18",
|
||||
"features": {}
|
||||
},
|
||||
"grave-domain": {
|
||||
"label": "Grave Domain",
|
||||
"source": "XGE pg. 19",
|
||||
"features": {}
|
||||
},
|
||||
"knowledge-domain": {
|
||||
"label": "Knowledge Domain",
|
||||
"source": "PHB pg. 59",
|
||||
"features": {}
|
||||
},
|
||||
"life-domain": {
|
||||
"label": "Life Domain",
|
||||
"source": "PHB pg. 60",
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.68bYIOvx6rIqnlOW", "Compendium.sw5e.classfeatures.jF8AFfEMICIJnAkR", "Compendium.sw5e.powers.8dzaICjGy6mTUaUr", "Compendium.sw5e.powers.uUWb1wZgtMou0TVP"],
|
||||
"2": ["Compendium.sw5e.classfeatures.hEymt45rICi4f9eL"],
|
||||
"3": ["Compendium.sw5e.powers.F0GsG0SJzsIOacwV", "Compendium.sw5e.powers.JbxsYXxSOTZbf9I0"],
|
||||
"5": ["Compendium.sw5e.powers.ZU9d6woBdUP8pIPt", "Compendium.sw5e.powers.LmRHHMtplpxr9fX6"],
|
||||
"6": ["Compendium.sw5e.classfeatures.yv49QN6Bzqs0ecCs"],
|
||||
"7": ["Compendium.sw5e.powers.VtCXMdyM6mAdIJZb", "Compendium.sw5e.powers.TgHsuhNasPbhu8MO"],
|
||||
"8": ["Compendium.sw5e.classfeatures.T6u5z8ZTX6UftXqE"],
|
||||
"9": ["Compendium.sw5e.powers.Pyzmm8R7rVsNAPsd", "Compendium.sw5e.powers.AGFMPAmuzwWO6Dfz"],
|
||||
"17": ["Compendium.sw5e.classfeatures.4UOgxzr83vFuUash"]
|
||||
}
|
||||
},
|
||||
"light-domain": {
|
||||
"label": "Light Domain",
|
||||
"source": "PHB pg. 60",
|
||||
"features": {}
|
||||
},
|
||||
"nature-domain": {
|
||||
"label": "Nature Domain",
|
||||
"source": "PHB pg. 61",
|
||||
"features": {}
|
||||
},
|
||||
"order-domain": {
|
||||
"label": "Order Domain",
|
||||
"source": "GGR pg. 25",
|
||||
"features": {}
|
||||
},
|
||||
"solidarity-domain": {
|
||||
"label": "Solidarity Domain",
|
||||
"source": "PS:A pg. 24",
|
||||
"features": {}
|
||||
},
|
||||
"strength-domain": {
|
||||
"label": "Strength Domain",
|
||||
"source": "PS:A pg. 25",
|
||||
"features": {}
|
||||
},
|
||||
"tempest-domain": {
|
||||
"label": "Tempest Domain",
|
||||
"source": "PHB pg. 62",
|
||||
"features": {}
|
||||
},
|
||||
"trickery-domain": {
|
||||
"label": "Trickery Domain",
|
||||
"source": "PHB pg. 62",
|
||||
"features": {}
|
||||
},
|
||||
"war-domain": {
|
||||
"label": "War Domain",
|
||||
"source": "PHB pg. 63",
|
||||
"features": {}
|
||||
},
|
||||
"zeal-domain": {
|
||||
"label": "Zeal Domain",
|
||||
"source": "PS:A pg. 28",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.x637K2Icp2ZFM1TB", "Compendium.sw5e.classfeatures.v4gKwLhAq9vuqza7"],
|
||||
"2": ["Compendium.sw5e.classfeatures.YpiLQEKGalROn7iJ"],
|
||||
"5": ["Compendium.sw5e.classfeatures.NMy4piwXIpLjYbRE"],
|
||||
"10": ["Compendium.sw5e.classfeatures.eVXqHn0ojWrEuYGU"]
|
||||
},
|
||||
},
|
||||
"druid": {
|
||||
"subclasses": {
|
||||
"circle-of-dreams": {
|
||||
"label": "Circle of Dreams",
|
||||
"source": "XGE pg. 22",
|
||||
"features": {}
|
||||
},
|
||||
"circle-of-the-land": {
|
||||
"label": "Circle of the Land",
|
||||
"source": "PHB pg. 68",
|
||||
"features": {
|
||||
"2": ["Compendium.sw5e.classfeatures.lT8GsPOPgRzDC3QJ", "Compendium.sw5e.classfeatures.wKdRtFsvGfMKQHLY"],
|
||||
"3": ["Compendium.sw5e.classfeatures.YiK59gWSlcQ6Mbdz"],
|
||||
"6": ["Compendium.sw5e.classfeatures.3FB25qKxmkmxcxuC"],
|
||||
"10": ["Compendium.sw5e.classfeatures.OTvrJSJSUgAwXrWX"],
|
||||
"14": ["Compendium.sw5e.classfeatures.EuX1kJNIw1F68yus"]
|
||||
}
|
||||
},
|
||||
"circle-of-the-moon": {
|
||||
"label": "Circle of the Moon",
|
||||
"source": "PHB pg. 69",
|
||||
"features": {}
|
||||
},
|
||||
"circle-of-the-shepherd": {
|
||||
"label": "Circle of the Shepherd",
|
||||
"source": "XGE pg. 23",
|
||||
"features": {}
|
||||
},
|
||||
"circle-of-spores": {
|
||||
"label": "Circle of Spores",
|
||||
"source": "GGR pg. 26",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.LzJ5ayHt0OlSVGxi", "Compendium.sw5e.classfeatures.i6tPm3FNK13Ftc9v"],
|
||||
"2": ["Compendium.sw5e.classfeatures.swK0r5TOIxredxWS", "Compendium.sw5e.classfeatures.u6Du2P9s81SWuGbi"],
|
||||
"18": ["Compendium.sw5e.classfeatures.cVDEQo0ow1WJT7Wl", "Compendium.sw5e.classfeatures.xvgPu1O57DgXCM86"],
|
||||
"20": ["Compendium.sw5e.classfeatures.ip4bvmGoz3qkoqes"]
|
||||
},
|
||||
},
|
||||
"fighter": {
|
||||
"subclasses": {
|
||||
"arcane-archer": {
|
||||
"label": "Arcane Archer",
|
||||
"source": "XGE pg. 28",
|
||||
"features": {}
|
||||
},
|
||||
"banneret": {
|
||||
"label": "Banneret",
|
||||
"source": "SCAG pg. 128",
|
||||
"features": {}
|
||||
},
|
||||
"battle-master": {
|
||||
"label": "Battle Master",
|
||||
"source": "PHB pg. 73",
|
||||
"features": {}
|
||||
},
|
||||
"cavalier": {
|
||||
"label": "Cavalier",
|
||||
"source": "XGE pg. 30",
|
||||
"features": {}
|
||||
},
|
||||
"champion": {
|
||||
"label": "Champion",
|
||||
"source": "PHB pg. 72",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.classfeatures.YgLQV1O849wE5TgM"],
|
||||
"7": ["Compendium.sw5e.classfeatures.dHu1yzIjD38BvGGd"],
|
||||
"11": ["Compendium.sw5e.classfeatures.kYJsED0rqqqUcgKz"],
|
||||
"15": ["Compendium.sw5e.classfeatures.aVKH6TLn1AG9hPSA"],
|
||||
"18": ["Compendium.sw5e.classfeatures.ipG5yx1tRNmeJfSH"]
|
||||
}
|
||||
},
|
||||
"echo-knight": {
|
||||
"label": "Echo Knight",
|
||||
"source": "EGW pg. 183",
|
||||
"features": {}
|
||||
},
|
||||
"eldritch-knight": {
|
||||
"label": "Eldritch Knight",
|
||||
"source": "PHB pg. 74",
|
||||
"features": {}
|
||||
},
|
||||
"samurai": {
|
||||
"label": "Samurai",
|
||||
"source": "XGE pg. 31",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.fbExzwNwEAl2kW9c", "Compendium.sw5e.classfeatures.nTjmWbyHweXuIqwc"],
|
||||
"2": ["Compendium.sw5e.classfeatures.xF1VTcJ3AdkbTsdQ"],
|
||||
"3": ["Compendium.sw5e.classfeatures.ax8M0X0q1GGWM26j"],
|
||||
"5": ["Compendium.sw5e.classfeatures.q9g1MLXuLZyxjQMg"],
|
||||
"9": ["Compendium.sw5e.classfeatures.653ZHbNcmm7ZGXbw"]
|
||||
},
|
||||
},
|
||||
"monk": {
|
||||
"subclasses": {
|
||||
"way-of-the-cobalt-soul": {
|
||||
"label": "Way of the Cobalt Soul",
|
||||
"source": "TCS pg. 104",
|
||||
"features": {}
|
||||
},
|
||||
"way-of-the-drunken-master": {
|
||||
"label": "Way of the Drunken Master",
|
||||
"source": "XGE pg. 33",
|
||||
"features": {}
|
||||
},
|
||||
"way-of-the-elements": {
|
||||
"label": "Way of the Four Elements",
|
||||
"source": "PHB pg. 80",
|
||||
"features": {}
|
||||
},
|
||||
"way-of-the-kensei": {
|
||||
"label": "Way of the Kensei",
|
||||
"source": "XGE pg. 34",
|
||||
"features": {}
|
||||
},
|
||||
"way-of-the-long-death": {
|
||||
"label": "Way of the Long Death",
|
||||
"source": "SCAG pg. 130",
|
||||
"features": {}
|
||||
},
|
||||
"way-of-the-open-hand": {
|
||||
"label": "Way of the Open Hand",
|
||||
"source": "PHB pg. 79",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.classfeatures.iQxLNydNLlCHNKbp"],
|
||||
"6": ["Compendium.sw5e.classfeatures.Q7mOdk4b1lgjcptF"],
|
||||
"11": ["Compendium.sw5e.classfeatures.rBDZLatuoolT2FUW"],
|
||||
"17": ["Compendium.sw5e.classfeatures.h1gM8SH3BNRtFevE"]
|
||||
}
|
||||
},
|
||||
"way-of-the-shadow": {
|
||||
"label": "Way of Shadow",
|
||||
"source": "PHB pg. 80",
|
||||
"features": {}
|
||||
},
|
||||
"way-of-the-sun-soul": {
|
||||
"label": "Way of the Sun Soul",
|
||||
"source": "XGE pg. 35; SCAG pg. 131",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.UAvV7N7T4zJhxdfI", "Compendium.sw5e.classfeatures.l50hjTxO2r0iecKw"],
|
||||
"2": ["Compendium.sw5e.classfeatures.10b6z2W1txNkrGP7", "Compendium.sw5e.classfeatures.7vSrGc0MP5Vzm9Ac"],
|
||||
"3": ["Compendium.sw5e.classfeatures.rtpQdX77dYWbDIOH", "Compendium.sw5e.classfeatures.mzweVbnsJPQiVkAe"],
|
||||
"4": ["Compendium.sw5e.classfeatures.KQz9bqxVkXjDl8gK"],
|
||||
"5": ["Compendium.sw5e.classfeatures.XogoBnFWmCAHXppo", "Compendium.sw5e.classfeatures.pvRc6GAu1ok6zihC"],
|
||||
"6": ["Compendium.sw5e.classfeatures.7flZKruSSu6dHg6D"],
|
||||
"7": ["Compendium.sw5e.classfeatures.a4P4DNMmH8CqSNkC", "Compendium.sw5e.classfeatures.ZmC31XKS4YNENnoc"],
|
||||
"10": ["Compendium.sw5e.classfeatures.bqWA7t9pDELbNRkp"],
|
||||
"13": ["Compendium.sw5e.classfeatures.XjuGBeB8Y0C3A5D4"],
|
||||
"14": ["Compendium.sw5e.classfeatures.7D2EkLdISwShEDlN"],
|
||||
"15": ["Compendium.sw5e.classfeatures.gDH8PMrKvLHaNmEI"],
|
||||
"18": ["Compendium.sw5e.classfeatures.3jwFt3hSqDswBlOH"],
|
||||
"20": ["Compendium.sw5e.classfeatures.mQNPg89YIs7g5tG4"]
|
||||
},
|
||||
},
|
||||
"paladin": {
|
||||
"subclasses": {
|
||||
"oath-of-the-ancients": {
|
||||
"label": "Oath of the Ancients",
|
||||
"source": "PHB pg. 86",
|
||||
"features": {}
|
||||
},
|
||||
"oath-of-conquest": {
|
||||
"label": "Oath of Conquest",
|
||||
"source": "SCAG pg. 128",
|
||||
"features": {}
|
||||
},
|
||||
"oath-of-the-crown": {
|
||||
"label": "Oath of the Crown",
|
||||
"source": "SCAG pg. 132",
|
||||
"features": {}
|
||||
},
|
||||
"oath-of-devotion": {
|
||||
"label": "Oath of Devotion",
|
||||
"source": "PHB pg. 85",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.powers.xmDBqZhRVrtLP8h2", "Compendium.sw5e.powers.gvdA9nPuWLck4tBl"],
|
||||
"5": ["Compendium.sw5e.powers.F0GsG0SJzsIOacwV", "Compendium.sw5e.powers.CylBa7jR8DSbo8Z3"],
|
||||
"9": ["Compendium.sw5e.powers.ZU9d6woBdUP8pIPt", "Compendium.sw5e.powers.15Fa6q1nH27XfbR8"],
|
||||
"13": ["Compendium.sw5e.powers.da0a1t2FqaTjRZGT", "Compendium.sw5e.powers.TgHsuhNasPbhu8MO"],
|
||||
"17": ["Compendium.sw5e.powers.d54VDyFulD9xxY7J", "Compendium.sw5e.powers.5e1xTohkzqFqbYH4"]
|
||||
}
|
||||
},
|
||||
"oathbreaker": {
|
||||
"label": "Oathbreaker",
|
||||
"source": "DMG pg. 97",
|
||||
"features": {}
|
||||
},
|
||||
"oath-of-redemption": {
|
||||
"label": "Oath of Redemption",
|
||||
"source": "XGE pg. 38",
|
||||
"features": {}
|
||||
},
|
||||
"oath-of-vengeance": {
|
||||
"label": "Oath of Vengeance",
|
||||
"source": "PHB pg. 87",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.E8ozg8avUVOX9N7u", "Compendium.sw5e.classfeatures.OdrvL3afwLOPeuYZ"],
|
||||
"2": ["Compendium.sw5e.classfeatures.ySMPQ6zNSlvkrl2f", "Compendium.sw5e.classfeatures.fbExzwNwEAl2kW9c", "Compendium.sw5e.classfeatures.ihoQHsmVZlyDbPhX"],
|
||||
"3": ["Compendium.sw5e.classfeatures.dY9yrqkyEDuF0CG2", "Compendium.sw5e.classfeatures.olAqNsUTIef9x8xC"],
|
||||
"5": ["Compendium.sw5e.classfeatures.XogoBnFWmCAHXppo"],
|
||||
"6": ["Compendium.sw5e.classfeatures.carGDhkIdoduTC0I"],
|
||||
"10": ["Compendium.sw5e.classfeatures.nahSkBO6LH4HkpaT"],
|
||||
"11": ["Compendium.sw5e.classfeatures.FAk41RPCTcvCk6KI"],
|
||||
"14": ["Compendium.sw5e.classfeatures.U7BIPVPsptBmwsnV"]
|
||||
},
|
||||
},
|
||||
"ranger": {
|
||||
"subclasses": {
|
||||
"beast-master": {
|
||||
"label": "Beast Master",
|
||||
"source": "PHB pg. 93",
|
||||
"features": {}
|
||||
},
|
||||
"gloom-stalker": {
|
||||
"label": "Gloom Stalker",
|
||||
"source": "XGE pg. 41",
|
||||
"features": {}
|
||||
},
|
||||
"horizon-walker": {
|
||||
"label": "Horizon Walker",
|
||||
"source": "XGE pg. 42",
|
||||
"features": {}
|
||||
},
|
||||
"hunter": {
|
||||
"label": "Hunter",
|
||||
"source": "PHB pg. 93",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.classfeatures.wrxIW5sDfmGr3u5s"],
|
||||
"7": ["Compendium.sw5e.classfeatures.WgQrqjmeyMqDzVt3"],
|
||||
"11": ["Compendium.sw5e.classfeatures.7zlTRRXT1vWSBGjX"],
|
||||
"15": ["Compendium.sw5e.classfeatures.a0Sq88dgnREcIMfl"]
|
||||
}
|
||||
},
|
||||
"monster-slayer": {
|
||||
"label": "Monster Slayer",
|
||||
"source": "XGE pg. 43",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.4Vpj9vCOB37GtXk6", "Compendium.sw5e.classfeatures.8fbZt2Qh7ZttwIan"],
|
||||
"2": ["Compendium.sw5e.classfeatures.fbExzwNwEAl2kW9c", "Compendium.sw5e.classfeatures.u6xV3Ki3TXRrD7zg"],
|
||||
"3": ["Compendium.sw5e.classfeatures.1dJHU48yNqn3lcfx", "Compendium.sw5e.classfeatures.kaHcUGiwi8AtfZIm"],
|
||||
"5": ["Compendium.sw5e.classfeatures.XogoBnFWmCAHXppo"],
|
||||
"8": ["Compendium.sw5e.classfeatures.C5fzaOBc6HxyOWRn"],
|
||||
"10": ["Compendium.sw5e.classfeatures.r0unvWK0lPsDthDx"],
|
||||
"14": ["Compendium.sw5e.classfeatures.DhU2dWCNnX78TstR"],
|
||||
"18": ["Compendium.sw5e.classfeatures.QBVmY56RMQuh6C8h"],
|
||||
"20": ["Compendium.sw5e.classfeatures.3CaP1vFHVR8LgHjx"]
|
||||
},
|
||||
},
|
||||
"rogue": {
|
||||
"subclasses": {
|
||||
"arcane-trickster": {
|
||||
"label": "Arcane Trickster",
|
||||
"source": "PHB pg. 97",
|
||||
"features": {}
|
||||
},
|
||||
"assassin": {
|
||||
"label": "Assassin",
|
||||
"source": "PHB pg. 97",
|
||||
"features": {}
|
||||
},
|
||||
"inquisitive": {
|
||||
"label": "Inquisitive",
|
||||
"source": "XGE pg. 45",
|
||||
"features": {}
|
||||
},
|
||||
"mastermind": {
|
||||
"label": "Mastermind",
|
||||
"source": "XGE pg. 46; SCAG pg. 135",
|
||||
"features": {}
|
||||
},
|
||||
"scout": {
|
||||
"label": "Scout",
|
||||
"source": "XGE pg. 47",
|
||||
"features": {}
|
||||
},
|
||||
"swashbuckler": {
|
||||
"label": "Swashbuckler",
|
||||
"source": "XGE pg. 47; SCAG pg. 135",
|
||||
"features": {}
|
||||
},
|
||||
"thief": {
|
||||
"label": "Thief",
|
||||
"source": "PHB pg. 97",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.classfeatures.ga3dt2zrCn2MHK8R", "Compendium.sw5e.classfeatures.FGrbXs6Ku5qxFK5G"],
|
||||
"9": ["Compendium.sw5e.classfeatures.Ei1Oh4UAA2E30jcD"],
|
||||
"13": ["Compendium.sw5e.classfeatures.NqWyHE7Rpw9lyKWu"],
|
||||
"17": ["Compendium.sw5e.classfeatures.LhRm1EeUMvp2EWhV"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.3sYPftQKnbbVnHrh", "Compendium.sw5e.classfeatures.DPN2Gfk8yi1Z5wp7", "Compendium.sw5e.classfeatures.ohwfuwnvuoBWlSQr"],
|
||||
"2": ["Compendium.sw5e.classfeatures.01pcLg6PRu5zGrsb"],
|
||||
"3": ["Compendium.sw5e.classfeatures.80USV8ZFPIahpLd0"],
|
||||
"5": ["Compendium.sw5e.classfeatures.Mm64SKAHJWYecgXS"],
|
||||
"7": ["Compendium.sw5e.classfeatures.a4P4DNMmH8CqSNkC"],
|
||||
"11": ["Compendium.sw5e.classfeatures.YN9xm6MCvse4Y60u"],
|
||||
"14": ["Compendium.sw5e.classfeatures.fjsBk7zxoAbLf8ZI"],
|
||||
"15": ["Compendium.sw5e.classfeatures.V4pwFxlwHtNeB4w9"],
|
||||
"18": ["Compendium.sw5e.classfeatures.L7nJSRosos8sHJH9"],
|
||||
"20": ["Compendium.sw5e.classfeatures.rQhWDaMHMn7iU4f2"]
|
||||
},
|
||||
},
|
||||
"sorcerer": {
|
||||
"subclasses": {
|
||||
"draconic-bloodline": {
|
||||
"label": "Draconic Bloodline",
|
||||
"source": "PHB pg. 102",
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.EZsonMThTNLZq35j", "Compendium.sw5e.classfeatures.MW1ExvBLm8Hg82aA"],
|
||||
"6": ["Compendium.sw5e.classfeatures.x6eEZ9GUsuOcEa3G"],
|
||||
"14": ["Compendium.sw5e.classfeatures.3647zjKSE9zFwOXc"],
|
||||
"18": ["Compendium.sw5e.classfeatures.Gsha4bl0apxqspFy"]
|
||||
}
|
||||
},
|
||||
"divine-soul": {
|
||||
"label": "Divine Soul",
|
||||
"source": "XGE pg. 50",
|
||||
"features": {}
|
||||
},
|
||||
"pyromancer": {
|
||||
"label": "Pyromancer",
|
||||
"source": "PS:K pg. 9",
|
||||
"features": {}
|
||||
},
|
||||
"runechild": {
|
||||
"label": "Runechild",
|
||||
"source": "TCS pg. 103",
|
||||
"features": {}
|
||||
},
|
||||
"shadow-magic": {
|
||||
"label": "Shadow Magic",
|
||||
"source": "XGE pg. 50",
|
||||
"features": {}
|
||||
},
|
||||
"storm-sorcery": {
|
||||
"label": "Storm Sorcery",
|
||||
"source": "XGE pg. 51; SCAG pg. 137",
|
||||
"features": {}
|
||||
},
|
||||
"wild-magic": {
|
||||
"label": "Wild Magic",
|
||||
"source": "PHB pg. 103",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.cmRCL9T9UgRYOj1c", "Compendium.sw5e.classfeatures.oygRF3ZjTv2T7z0Y"],
|
||||
"2": ["Compendium.sw5e.classfeatures.LBKChJY5n02Afhnq"],
|
||||
"3": ["Compendium.sw5e.classfeatures.9Uh7uTDNZ04oTJsL"],
|
||||
"20": ["Compendium.sw5e.classfeatures.F2lEKSmOY0NUruzY"]
|
||||
},
|
||||
},
|
||||
"warlock": {
|
||||
"subclasses": {
|
||||
"the-archfey": {
|
||||
"label": "The Archfey",
|
||||
"source": "PHB pg. 108",
|
||||
"features": {}
|
||||
},
|
||||
"the-celestial": {
|
||||
"label": "The Celestial",
|
||||
"source": "XGE pg. 54",
|
||||
"features": {}
|
||||
},
|
||||
"the-fiend": {
|
||||
"label": "The Fiend",
|
||||
"source": "PHB pg. 109",
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.Jv0zu4BtUi8bFCqJ"],
|
||||
"6": ["Compendium.sw5e.classfeatures.OQSb0bO1yDI4aiMx"],
|
||||
"10": ["Compendium.sw5e.classfeatures.9UZ2WjUF2k58CQug"],
|
||||
"14": ["Compendium.sw5e.classfeatures.aCUmlnHlUPHS0rdu"]
|
||||
}
|
||||
},
|
||||
"the-hexblade": {
|
||||
"label": "The Hexblade",
|
||||
"source": "XGE pg. 55",
|
||||
"features": {}
|
||||
},
|
||||
"the-oldone": {
|
||||
"label": "The Great Old One",
|
||||
"source": "PHB pg. 109",
|
||||
"features": {}
|
||||
},
|
||||
"the-undying": {
|
||||
"label": "The Undying",
|
||||
"source": "SCAG pg. 139",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.jTXHaK0vvT5DV3uO", "Compendium.sw5e.classfeatures.x6IJZwr6f0SGral7"],
|
||||
"2": ["Compendium.sw5e.classfeatures.8MlxM2nEfE3Q0EVk"],
|
||||
"3": ["Compendium.sw5e.classfeatures.QwgfIpCN8VWfoUtX"],
|
||||
"11": ["Compendium.sw5e.classfeatures.zB77V8BcCJvWVxck"],
|
||||
"13": ["Compendium.sw5e.classfeatures.HBn6FXLz7Eiudz0V"],
|
||||
"15": ["Compendium.sw5e.classfeatures.knDZR4l4QfLTKinm"],
|
||||
"17": ["Compendium.sw5e.classfeatures.vMxJQEKeK6WwZFaF"],
|
||||
"20": ["Compendium.sw5e.classfeatures.0C04rwyvoknvFYiy"]
|
||||
},
|
||||
},
|
||||
"wizard": {
|
||||
"subclasses": {
|
||||
"school-of-abjuration": {
|
||||
"label": "School of Abjuration",
|
||||
"source": "PHB pg. 115",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-bladesinging": {
|
||||
"label": "School of Bladesinging",
|
||||
"source": "SCAG pg. 141",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-chronurgy-magic": {
|
||||
"label": "School of Chronurgy Magic",
|
||||
"source": "EGW pg. 185",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-conjuration": {
|
||||
"label": "School of Conjuration",
|
||||
"source": "PHB pg. 116",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-divination": {
|
||||
"label": "School of Divination",
|
||||
"source": "PHB pg. 116",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-enchantment": {
|
||||
"label": "School of Enchantment",
|
||||
"source": "PHB pg. 117",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-evocation": {
|
||||
"label": "School of Evocation",
|
||||
"source": "PHB pg. 117",
|
||||
"features": {
|
||||
"2": ["Compendium.sw5e.classfeatures.7uzJ2JkmsdRGLra3", "Compendium.sw5e.classfeatures.6VBXkjjBgjSpNElh"],
|
||||
"6": ["Compendium.sw5e.classfeatures.evEWCpE5MYgr5RRW"],
|
||||
"10": ["Compendium.sw5e.classfeatures.7O85kj6uDEG5NzUE"],
|
||||
"14": ["Compendium.sw5e.classfeatures.VUtSLeCzFubGXmGx"]
|
||||
}
|
||||
},
|
||||
"school-of-graviturgy-magic": {
|
||||
"label": "School of Graviturgy Magic",
|
||||
"source": "EGW pg. 185",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-illusion": {
|
||||
"label": "School of Illusion",
|
||||
"source": "PHB pg. 118",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-necromancy": {
|
||||
"label": "School of Necromancy",
|
||||
"source": "PHB pg. 118",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-transmutation": {
|
||||
"label": "School of Transmutation",
|
||||
"source": "PHB pg. 119",
|
||||
"features": {}
|
||||
},
|
||||
"school-of-war-magic": {
|
||||
"label": "School of War Magic",
|
||||
"source": "XGE pg. 59",
|
||||
"features": {}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.gbNo5eVPaqr8IVKL", "Compendium.sw5e.classfeatures.e0uTcFPpgxjIyUW9"],
|
||||
"2": ["Compendium.sw5e.classfeatures.AEWr9EMxy5gj4ZFT"],
|
||||
"18": ["Compendium.sw5e.classfeatures.JfFfHTeIszx1hNRx"],
|
||||
"20": ["Compendium.sw5e.classfeatures.nUrZDi6QN1YjwAr6", "Compendium.sw5e.classfeatures.31bKbWe9ZGVLEns6"]
|
||||
},
|
||||
}
|
||||
*/
|
||||
};
|
|
@ -9,8 +9,17 @@ export const _getInitiativeFormula = function(combatant) {
|
|||
const actor = combatant.actor;
|
||||
if ( !actor ) return "1d20";
|
||||
const init = actor.data.data.attributes.init;
|
||||
const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
||||
if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh";
|
||||
|
||||
let nd = 1;
|
||||
let mods = "";
|
||||
|
||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r=1";
|
||||
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
||||
nd = 2;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
||||
|
||||
// Optionally apply Dexterity tiebreaker
|
||||
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {ClassFeatures} from "./classFeatures.js"
|
||||
|
||||
// Namespace SW5e Configuration Values
|
||||
export const SW5E = {};
|
||||
|
||||
|
@ -282,6 +284,9 @@ SW5E.damageTypes = {
|
|||
"sonic": "SW5E.DamageSonic"
|
||||
};
|
||||
|
||||
// Damage Resistance Types
|
||||
SW5E.damageResistanceTypes = duplicate(SW5E.damageTypes);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// armor Types
|
||||
|
@ -291,6 +296,7 @@ SW5E.armorPropertiesTypes = {
|
|||
"Anchor": "SW5E.ArmorProperAnchor",
|
||||
"Avoidant": "SW5E.ArmorProperAvoidant",
|
||||
"Barbed": "SW5E.ArmorProperBarbed",
|
||||
"Bulky": "SW5E.ArmorProperBulky",
|
||||
"Charging": "SW5E.ArmorProperCharging",
|
||||
"Concealing": "SW5E.ArmorProperConcealing",
|
||||
"Cumbersome": "SW5E.ArmorProperCumbersome",
|
||||
|
@ -303,6 +309,7 @@ SW5E.armorPropertiesTypes = {
|
|||
"Lightweight": "SW5E.ArmorProperLightweight",
|
||||
"Magnetic": "SW5E.ArmorProperMagnetic",
|
||||
"Obscured": "SW5E.ArmorProperObscured",
|
||||
"Obtrusive": "SW5E.ArmorProperObtrusive",
|
||||
"Powered": "SW5E.ArmorProperPowered",
|
||||
"Reactive": "SW5E.ArmorProperReactive",
|
||||
"Regulated": "SW5E.ArmorProperRegulated",
|
||||
|
@ -311,6 +318,7 @@ SW5E.armorPropertiesTypes = {
|
|||
"Rigid": "SW5E.ArmorProperRigid",
|
||||
"Silent": "SW5E.ArmorProperSilent",
|
||||
"Spiked": "SW5E.ArmorProperSpiked",
|
||||
"Strength": "SW5E.ArmorProperStrength",
|
||||
"Steadfast": "SW5E.ArmorProperSteadfast",
|
||||
"Versatile": "SW5E.ArmorProperVersatile"
|
||||
};
|
||||
|
@ -490,7 +498,9 @@ SW5E.weaponTypes = {
|
|||
"martialLW": "SW5E.WeaponMartialLW",
|
||||
"natural": "SW5E.WeaponNatural",
|
||||
"improv": "SW5E.WeaponImprov",
|
||||
"ammo": "SW5E.WeaponAmmo"
|
||||
"ammo": "SW5E.WeaponAmmo",
|
||||
"siege": "SW5E.WeaponSiege"
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -512,14 +522,15 @@ SW5E.weaponProperties = {
|
|||
"dis": "SW5E.WeaponPropertiesDis",
|
||||
"dpt": "SW5E.WeaponPropertiesDpt",
|
||||
"dou": "SW5E.WeaponPropertiesDou",
|
||||
"hvy": "SW5E.WeaponPropertiesHvy",
|
||||
"hid": "SW5E.WeaponPropertiesHid",
|
||||
"fin": "SW5E.WeaponPropertiesFin",
|
||||
"fix": "SW5E.WeaponPropertiesFix",
|
||||
"foc": "SW5E.WeaponPropertiesFoc",
|
||||
"hvy": "SW5E.WeaponPropertiesHvy",
|
||||
"hid": "SW5E.WeaponPropertiesHid",
|
||||
"ken": "SW5E.WeaponPropertiesKen",
|
||||
"lgt": "SW5E.WeaponPropertiesLgt",
|
||||
"lum": "SW5E.WeaponPropertiesLum",
|
||||
"mig": "SW5E.WeaponPropertiesMig",
|
||||
"pic": "SW5E.WeaponPropertiesPic",
|
||||
"rap": "SW5E.WeaponPropertiesRap",
|
||||
"rch": "SW5E.WeaponPropertiesRch",
|
||||
|
@ -552,7 +563,6 @@ SW5E.powerSchools = {
|
|||
"enh": "SW5E.SchoolEnh"
|
||||
};
|
||||
|
||||
|
||||
// Power Levels
|
||||
SW5E.powerLevels = {
|
||||
0: "SW5E.PowerLevel0",
|
||||
|
@ -639,7 +649,6 @@ SW5E.cover = {
|
|||
.5: 'SW5E.CoverHalf',
|
||||
.75: 'SW5E.CoverThreeQuarters',
|
||||
1: 'SW5E.CoverTotal'
|
||||
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -662,6 +671,7 @@ SW5E.conditionTypes = {
|
|||
"prone": "SW5E.ConProne",
|
||||
"restrained": "SW5E.ConRestrained",
|
||||
"shocked": "SW5E.ConShocked",
|
||||
"slowed": "SW5E.ConSlowed",
|
||||
"stunned": "SW5E.ConStunned",
|
||||
"unconscious": "SW5E.ConUnconscious"
|
||||
};
|
||||
|
@ -782,6 +792,9 @@ SW5E.CR_EXP_LEVELS = [
|
|||
20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
|
||||
];
|
||||
|
||||
// Character Features Per Class And Level
|
||||
SW5E.classFeatures = ClassFeatures;
|
||||
|
||||
// Configure Optional Character Flags
|
||||
SW5E.characterFlags = {
|
||||
"detailOriented": {
|
||||
|
@ -863,7 +876,13 @@ SW5E.characterFlags = {
|
|||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"remarkableAthlete": {
|
||||
"reliableTalent": {
|
||||
name: "SW5E.FlagsReliableTalent",
|
||||
hint: "SW5E.FlagsReliableTalentHint",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"remarkableAthlete": {
|
||||
name: "SW5E.FlagsRemarkableAthlete",
|
||||
hint: "SW5E.FlagsRemarkableAthleteHint",
|
||||
abilities: ['str','dex','con'],
|
||||
|
@ -878,3 +897,8 @@ SW5E.characterFlags = {
|
|||
placeholder: 20
|
||||
}
|
||||
};
|
||||
|
||||
// Configure allowed status flags
|
||||
SW5E.allowedActorFlags = [
|
||||
"isPolymorphed", "originalActor"
|
||||
].concat(Object.keys(SW5E.characterFlags));
|
||||
|
|
|
@ -150,7 +150,6 @@ export default class Item5e extends Item {
|
|||
|
||||
// Get the Item's data
|
||||
const itemData = this.data;
|
||||
const actorData = this.actor ? this.actor.data : {};
|
||||
const data = itemData.data;
|
||||
const C = CONFIG.SW5E;
|
||||
const labels = {};
|
||||
|
@ -228,16 +227,12 @@ export default class Item5e extends Item {
|
|||
// Item Actions
|
||||
if ( data.hasOwnProperty("actionType") ) {
|
||||
|
||||
// Save DC
|
||||
let save = data.save || {};
|
||||
if ( !save.ability ) save.dc = null;
|
||||
else if ( this.isOwned ) { // Actor owned items
|
||||
if ( save.scaling === "power" ) save.dc = actorData.data.attributes.powerdc;
|
||||
else if ( save.scaling !== "flat" ) save.dc = this.actor.getPowerDC(save.scaling);
|
||||
} else { // Un-owned items
|
||||
// Saving throws for unowned items
|
||||
const save = data.save;
|
||||
if ( save?.ability && !this.isOwned ) {
|
||||
if ( save.scaling !== "flat" ) save.dc = null;
|
||||
labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: C.abilities[save.ability]});
|
||||
}
|
||||
labels.save = save.ability ? `${game.i18n.localize("SW5E.AbbreviationDC")} ${save.dc || ""} ${C.abilities[save.ability]}` : "";
|
||||
|
||||
// Damage
|
||||
let dam = data.damage || {};
|
||||
|
@ -303,13 +298,20 @@ export default class Item5e extends Item {
|
|||
user: game.user._id,
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
|
||||
content: html,
|
||||
flavor: this.name,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
}
|
||||
},
|
||||
flags: {"core.canPopout": true}
|
||||
};
|
||||
|
||||
// If the consumable was destroyed in the process - embed the item data in the surviving message
|
||||
if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) {
|
||||
chatData.flags["sw5e.itemData"] = this.data;
|
||||
}
|
||||
|
||||
// Toggle default roll mode
|
||||
rollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
|
||||
|
@ -443,7 +445,7 @@ export default class Item5e extends Item {
|
|||
// Maybe initiate template placement workflow
|
||||
if ( this.hasAreaTarget && placeTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(this);
|
||||
if ( template ) template.drawPreview(event);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
|
@ -481,7 +483,7 @@ export default class Item5e extends Item {
|
|||
// Ability activation properties
|
||||
if ( data.hasOwnProperty("activation") ) {
|
||||
props.push(
|
||||
labels.activation + (data.activation.condition ? `(${data.activation.condition})` : ""),
|
||||
labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""),
|
||||
labels.target,
|
||||
labels.range,
|
||||
labels.duration
|
||||
|
@ -617,17 +619,15 @@ export default class Item5e extends Item {
|
|||
rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + ");
|
||||
}
|
||||
|
||||
// Ammunition Bonus
|
||||
delete this._ammo;
|
||||
const consume = itemData.consume;
|
||||
if ( consume?.type === "ammo" ) {
|
||||
if ( !consume.target ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
const ammo = this.actor.items.get(consume.target);
|
||||
// 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;
|
||||
if ( q && (q - consume.amount >= 0) ) {
|
||||
const consumeAmount = consume.amount ?? 0;
|
||||
if ( q && (q - consumeAmount >= 0) ) {
|
||||
let ammoBonus = ammo.data.data.attackBonus;
|
||||
if ( ammoBonus ) {
|
||||
parts.push("@ammo");
|
||||
|
@ -636,7 +636,10 @@ export default class Item5e extends Item {
|
|||
this._ammo = ammo;
|
||||
}
|
||||
}
|
||||
//}else{
|
||||
// ui.notifications.error(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
|
||||
}
|
||||
}
|
||||
|
||||
// Compose roll options
|
||||
const rollConfig = mergeObject({
|
||||
|
@ -856,7 +859,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
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
async rollFormula(options={}) {
|
||||
if ( !this.data.data.formula ) {
|
||||
|
@ -941,7 +944,7 @@ export default class Item5e extends Item {
|
|||
// Maybe initiate template placement workflow
|
||||
if ( this.hasAreaTarget && placeTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(this);
|
||||
if ( template ) template.drawPreview(event);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
|
@ -1065,48 +1068,41 @@ export default class Item5e extends Item {
|
|||
const isTargetted = action === "save";
|
||||
if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return;
|
||||
|
||||
// Get the Actor from a synthetic Token
|
||||
// Recover the actor for the chat card
|
||||
const actor = this._getChatCardActor(card);
|
||||
if ( !actor ) return;
|
||||
|
||||
// Get the Item
|
||||
const item = actor.getOwnedItem(card.dataset.itemId);
|
||||
// Get the Item from stored flag data or by the item ID on the Actor
|
||||
const storedData = message.getFlag("sw5e", "itemData");
|
||||
const item = storedData ? this.createOwned(storedData, actor) : actor.getOwnedItem(card.dataset.itemId);
|
||||
if ( !item ) {
|
||||
return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}))
|
||||
}
|
||||
const powerLevel = parseInt(card.dataset.powerLevel) || null;
|
||||
|
||||
// Get card targets
|
||||
let targets = [];
|
||||
if ( isTargetted ) {
|
||||
targets = this._getChatCardTargets(card);
|
||||
if ( !targets.length ) {
|
||||
ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
|
||||
return button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Attack and Damage Rolls
|
||||
if ( action === "attack" ) await item.rollAttack({event});
|
||||
else if ( action === "damage" ) await item.rollDamage({event, powerLevel});
|
||||
else if ( action === "versatile" ) await item.rollDamage({event, powerLevel, versatile: true});
|
||||
else if ( action === "formula" ) await item.rollFormula({event, powerLevel});
|
||||
|
||||
// Saving Throws for card targets
|
||||
else if ( action === "save" ) {
|
||||
for ( let a of targets ) {
|
||||
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: a.token});
|
||||
await a.rollAbilitySave(button.dataset.ability, { event, speaker });
|
||||
}
|
||||
}
|
||||
|
||||
// Tool usage
|
||||
else if ( action === "toolCheck" ) await item.rollToolCheck({event});
|
||||
|
||||
// Power Template Creation
|
||||
else if ( action === "placeTemplate") {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview(event);
|
||||
// Handle different actions
|
||||
switch ( action ) {
|
||||
case "attack":
|
||||
await item.rollAttack({event}); break;
|
||||
case "damage":
|
||||
await item.rollDamage({event, powerLevel}); break;
|
||||
case "versatile":
|
||||
await item.rollDamage({event, powerLevel, versatile: true}); break;
|
||||
case "formula":
|
||||
await item.rollFormula({event, powerLevel}); break;
|
||||
case "save":
|
||||
const targets = this._getChatCardTargets(card);
|
||||
for ( let token of targets ) {
|
||||
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token});
|
||||
await token.actor.rollAbilitySave(button.dataset.ability, { event, speaker });
|
||||
}
|
||||
break;
|
||||
case "toolCheck":
|
||||
await item.rollToolCheck({event}); break;
|
||||
case "placeTemplate":
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview();
|
||||
break;
|
||||
}
|
||||
|
||||
// Re-enable the button
|
||||
|
@ -1164,10 +1160,9 @@ export default class Item5e extends Item {
|
|||
* @private
|
||||
*/
|
||||
static _getChatCardTargets(card) {
|
||||
const character = game.user.character;
|
||||
const controlled = canvas.tokens.controlled;
|
||||
const targets = controlled.reduce((arr, t) => t.actor ? arr.concat([t.actor]) : arr, []);
|
||||
if ( character && (controlled.length === 0) ) targets.push(character);
|
||||
let targets = canvas.tokens.controlled.filter(t => !!t.actor);
|
||||
if ( !targets.length && game.user.character ) targets = targets.concat(game.user.character.getActiveTokens());
|
||||
if ( !targets.length ) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
|
||||
return targets;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
constructor(...args) {
|
||||
super(...args);
|
||||
if ( this.object.data.type === "class" ) {
|
||||
this.options.resizable = true;
|
||||
this.options.width = 600;
|
||||
this.options.height = 640;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +18,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: 420,
|
||||
height: "auto",
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
resizable: true,
|
||||
scrollY: [".tab.details"],
|
||||
|
@ -220,7 +218,9 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/** @override */
|
||||
setPosition(position={}) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
if ( !this._minimized ) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
}
|
||||
return super.setPosition(position);
|
||||
}
|
||||
|
||||
|
@ -249,13 +249,6 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
super.activateListeners(html);
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
|
||||
|
||||
// Armor properties
|
||||
// html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
|
||||
|
||||
// Weapon properties
|
||||
// html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this));
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -136,6 +136,34 @@ export const migrateActorData = function(actor) {
|
|||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
|
||||
* @param {Object} actorData The data object for an Actor
|
||||
* @return {Object} The scrubbed Actor data
|
||||
*/
|
||||
function cleanActorData(actorData) {
|
||||
|
||||
// Scrub system data
|
||||
const model = game.system.model.Actor[actorData.type];
|
||||
actorData.data = filterObject(actorData.data, model);
|
||||
|
||||
// Scrub system flags
|
||||
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
|
||||
obj[f] = null;
|
||||
return obj;
|
||||
}, {});
|
||||
if ( actorData.flags.sw5e ) {
|
||||
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
|
||||
}
|
||||
|
||||
// Return the scrubbed data
|
||||
return actorData;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
|
|
@ -52,9 +52,8 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
|
||||
/**
|
||||
* Creates a preview of the power template
|
||||
* @param {Event} event The initiating click event
|
||||
*/
|
||||
drawPreview(event) {
|
||||
drawPreview() {
|
||||
const initialLayer = canvas.activeLayer;
|
||||
this.draw();
|
||||
this.layer.activate();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue