diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..4ca0a625
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,14 @@
+{
+ "printWidth": 120,
+ "tabWidth": 4,
+ "useTabs": false,
+ "semi": true,
+ "singleQuote": false,
+ "quoteProps": "consistent",
+ "jsxSingleQuote": false,
+ "trailingComma": "none",
+ "bracketSpacing": false,
+ "jsxBracketSameLine": false,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7a73a41b..cac0e10e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,2 +1,3 @@
{
+ "editor.formatOnSave": true
}
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
index dabc0757..d18fc7d3 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -8,19 +8,19 @@ const less = require("gulp-less");
const SW5E_LESS = ["less/**/*.less"];
function compileLESS() {
- return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
+ return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
}
function compileGlobalLess() {
- return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
+ return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
}
function compileLightLess() {
- return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
+ return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
}
function compileDarkLess() {
- return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
+ return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
}
const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
@@ -30,7 +30,7 @@ const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compil
/* ----------------------------------------- */
function watchUpdates() {
- gulp.watch(SW5E_LESS, css);
+ gulp.watch(SW5E_LESS, css);
}
/* ----------------------------------------- */
diff --git a/module/actor/entity.js b/module/actor/entity.js
index 0b22844c..0fe01203 100644
--- a/module/actor/entity.js
+++ b/module/actor/entity.js
@@ -1,8 +1,8 @@
-import { d20Roll, damageRoll } from "../dice.js";
+import {d20Roll, damageRoll} from "../dice.js";
import SelectItemsPrompt from "../apps/select-items-prompt.js";
import ShortRestDialog from "../apps/short-rest.js";
import LongRestDialog from "../apps/long-rest.js";
-import {SW5E} from '../config.js';
+import {SW5E} from "../config.js";
import Item5e from "../item/entity.js";
/**
@@ -10,1813 +10,1875 @@ import Item5e from "../item/entity.js";
* @extends {Actor}
*/
export default class Actor5e extends Actor {
+ /**
+ * The data source for Actor5e.classes allowing it to be lazily computed.
+ * @type {Object}
+ * @private
+ */
+ _classes = undefined;
- /**
- * The data source for Actor5e.classes allowing it to be lazily computed.
- * @type {Object}
- * @private
- */
- _classes = undefined;
+ /* -------------------------------------------- */
+ /* Properties */
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
- /* Properties */
- /* -------------------------------------------- */
-
- /**
- * A mapping of classes belonging to this Actor.
- * @type {Object}
- */
- get classes() {
- if ( this._classes !== undefined ) return this._classes;
- if ( this.data.type !== "character" ) return this._classes = {};
- return this._classes = this.items.filter((item) => item.type === "class").reduce((obj, cls) => {
- obj[cls.name.slugify({strict: true})] = cls;
- return obj;
- }, {});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Is this Actor currently polymorphed into some other creature?
- * @type {boolean}
- */
- get isPolymorphed() {
- return this.getFlag("sw5e", "isPolymorphed") || false;
- }
-
- /* -------------------------------------------- */
- /* Methods */
- /* -------------------------------------------- */
-
- /** @override */
- prepareData() {
- super.prepareData();
-
- // iterate over owned items and recompute attributes that depend on prepared actor data
- this.items.forEach(item => item.prepareFinalAttributes());
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- prepareBaseData() {
- switch ( this.data.type ) {
- case "character":
- return this._prepareCharacterData(this.data);
- case "npc":
- return this._prepareNPCData(this.data);
- case "starship":
- return this._prepareStarshipData(this.data);
- case "vehicle":
- return this._prepareVehicleData(this.data);
+ /**
+ * A mapping of classes belonging to this Actor.
+ * @type {Object}
+ */
+ get classes() {
+ if (this._classes !== undefined) return this._classes;
+ if (this.data.type !== "character") return (this._classes = {});
+ return (this._classes = this.items
+ .filter((item) => item.type === "class")
+ .reduce((obj, cls) => {
+ obj[cls.name.slugify({strict: true})] = cls;
+ return obj;
+ }, {}));
}
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- prepareDerivedData() {
- const actorData = this.data;
- const data = actorData.data;
- const flags = actorData.flags.sw5e || {};
- const bonuses = getProperty(data, "bonuses.abilities") || {};
+ /**
+ * Is this Actor currently polymorphed into some other creature?
+ * @type {boolean}
+ */
+ get isPolymorphed() {
+ return this.getFlag("sw5e", "isPolymorphed") || false;
+ }
- // Retrieve data for polymorphed actors
- let originalSaves = null;
- let originalSkills = null;
- if (this.isPolymorphed) {
- const transformOptions = this.getFlag('sw5e', 'transformOptions');
- const original = game.actors?.get(this.getFlag('sw5e', 'originalActor'));
- if (original) {
- if (transformOptions.mergeSaves) {
- originalSaves = original.data.data.abilities;
+ /* -------------------------------------------- */
+ /* Methods */
+ /* -------------------------------------------- */
+
+ /** @override */
+ prepareData() {
+ super.prepareData();
+
+ // iterate over owned items and recompute attributes that depend on prepared actor data
+ this.items.forEach((item) => item.prepareFinalAttributes());
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ prepareBaseData() {
+ switch (this.data.type) {
+ case "character":
+ return this._prepareCharacterData(this.data);
+ case "npc":
+ return this._prepareNPCData(this.data);
+ case "starship":
+ return this._prepareStarshipData(this.data);
+ case "vehicle":
+ return this._prepareVehicleData(this.data);
}
- if (transformOptions.mergeSkills) {
- originalSkills = original.data.data.skills;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ prepareDerivedData() {
+ const actorData = this.data;
+ const data = actorData.data;
+ const flags = actorData.flags.sw5e || {};
+ const bonuses = getProperty(data, "bonuses.abilities") || {};
+
+ // Retrieve data for polymorphed actors
+ let originalSaves = null;
+ let originalSkills = null;
+ if (this.isPolymorphed) {
+ const transformOptions = this.getFlag("sw5e", "transformOptions");
+ const original = game.actors?.get(this.getFlag("sw5e", "originalActor"));
+ if (original) {
+ if (transformOptions.mergeSaves) {
+ originalSaves = original.data.data.abilities;
+ }
+ if (transformOptions.mergeSkills) {
+ originalSkills = original.data.data.skills;
+ }
+ }
}
- }
- }
- // 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)) {
- abl.mod = Math.floor((abl.value - 10) / 2);
- abl.prof = (abl.proficient || 0) * data.attributes.prof;
- abl.saveBonus = saveBonus;
- abl.checkBonus = checkBonus;
- abl.save = abl.mod + abl.prof + abl.saveBonus;
- abl.dc = 8 + abl.mod + data.attributes.prof + dcBonus;
-
- // If we merged saves when transforming, take the highest bonus here.
- if (originalSaves && abl.proficient) {
- 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);
-
- // Reset class store to ensure it is updated with any changes
- this._classes = undefined;
-
- // Determine Initiative Modifier
- const init = data.attributes.init;
- const athlete = flags.remarkableAthlete;
- const joat = flags.jackOfAllTrades;
- init.mod = data.abilities.dex.mod;
- 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.value = init.value ?? 0;
- init.bonus = init.value + (flags.initiativeAlert ? 5 : 0);
- init.total = init.mod + init.prof + init.bonus;
-
- // Cache labels
- this.labels = {};
- if ( this.type === "npc" ) {
- this.labels["creatureType"] = this.constructor.formatCreatureType(data.details.type);
- }
-
- // Prepare power-casting data
- this._computeDerivedPowercasting(this.data);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return the amount of experience required to gain a certain character level.
- * @param level {Number} The desired level
- * @return {Number} The XP required
- */
- getLevelExp(level) {
- const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS;
- return levels[Math.min(level, levels.length - 1)];
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return the amount of experience granted by killing a creature of a certain CR.
- * @param cr {Number} The creature's challenge rating
- * @return {Number} The amount of experience granted per kill
- */
- getCRExp(cr) {
- if (cr < 1.0) return Math.max(200 * cr, 10);
- return CONFIG.SW5E.CR_EXP_LEVELS[cr];
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- getRollData() {
- const data = super.getRollData();
- data.prof = this.data.data.attributes.prof || 0;
- data.classes = Object.entries(this.classes).reduce((obj, e) => {
- const [slug, cls] = e;
- obj[slug] = cls.data.data;
- return obj;
- }, {});
- return data;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Given a list of items to add to the Actor, optionally prompt the
- * user for which they would like to add.
- * @param {Array.} items - The items being added to the Actor.
- * @param {boolean} [prompt=true] - Whether or not to prompt the user.
- * @returns {Promise}
- */
- async addEmbeddedItems(items, prompt=true) {
- let itemsToAdd = items;
- if ( !items.length ) return [];
-
- // Obtain the array of item creation data
- let toCreate = [];
- if (prompt) {
- const itemIdsToAdd = await SelectItemsPrompt.create(items, {
- hint: game.i18n.localize('SW5E.AddEmbeddedItemPromptHint')
- });
- for (let item of items) {
- if (itemIdsToAdd.includes(item.id)) toCreate.push(item.toObject());
- }
- } else {
- toCreate = items.map(item => item.toObject());
- }
-
- // Create the requested items
- if (itemsToAdd.length === 0) return [];
- return Item5e.createDocuments(toCreate, {parent: this});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Get a list of features to add to the Actor when a class item is updated.
- * Optionally prompt the user for which they would like to add.
- */
- async getClassFeatures({className, archetypeName, level}={}) {
- const existing = new Set(this.items.map(i => i.name));
- const features = await Actor5e.loadClassFeatures({className, archetypeName, level});
- return features.filter(f => !existing.has(f.name)) || [];
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return the features which a character is awarded for each class level
- * @param {string} className The class name being added
- * @param {string} archetypeName The archetype 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} Array of Item5e entities
- */
- static async loadClassFeatures({className="", archetypeName="", level=1, priorLevel=0}={}) {
- className = className.toLowerCase();
- archetypeName = archetypeName.slugify();
-
- // Get the configuration of features which may be added
- const clsConfig = CONFIG.SW5E.classFeatures[className];
- if (!clsConfig) return [];
-
- // 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 archetype features
- const archConfig = clsConfig.archetypes[archetypeName] || {};
- for ( let [l, f] of Object.entries(archConfig.features || {}) ) {
- l = parseInt(l);
- if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
- }
-
- // Load item data for all identified features
- const features = [];
- for ( let id of ids ) {
- features.push(await 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;
- }
-
- /* -------------------------------------------- */
- /* Data Preparation Helpers */
- /* -------------------------------------------- */
-
- /**
- * Prepare Character type specific data
- */
- _prepareCharacterData(actorData) {
- const data = actorData.data;
-
- // Determine character level and available hit dice based on owned Class items
- const [level, hd] = this.items.reduce((arr, item) => {
- if ( item.type === "class" ) {
- const classLevels = parseInt(item.data.data.levels) || 1;
- arr[0] += classLevels;
- arr[1] += classLevels - (parseInt(item.data.data.hitDiceUsed) || 0);
- }
- return arr;
- }, [0, 0]);
- data.details.level = level;
- data.attributes.hd = hd;
-
- // Character proficiency bonus
- data.attributes.prof = Math.floor((level + 7) / 4);
-
- // Experience required for next level
- const xp = data.details.xp;
- xp.max = this.getLevelExp(level || 1);
- const prior = this.getLevelExp(level - 1 || 0);
- const required = xp.max - prior;
- const pct = Math.round((xp.value - prior) * 100 / required);
- xp.pct = Math.clamped(pct, 0, 100);
-
- // Add base Powercasting attributes
- this._computeBasePowercasting(actorData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare NPC type specific data
- */
- _prepareNPCData(actorData) {
- const data = actorData.data;
-
- // Kill Experience
- data.details.xp.value = this.getCRExp(data.details.cr);
-
- // Proficiency
- data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4);
-
- this._computeBasePowercasting(actorData);
-
- // Powercaster Level
- if ( data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel) ) {
- data.details.powerLevel = Math.max(data.details.cr, 1);
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare vehicle type-specific data
- * @param actorData
- * @private
- */
- _prepareVehicleData(actorData) {}
-
- /* -------------------------------------------- */
-
- /**
- * Prepare starship type-specific data
- * @param actorData
- * @private
- */
- _prepareStarshipData(actorData) {
- const data = actorData.data;
-
- // Proficiency
- data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4);
-
- // Link hull to hp and shields to temp hp
- data.attributes.hull.value = data.attributes.hp.value;
- data.attributes.hull.max = data.attributes.hp.max;
- data.attributes.shld.value = data.attributes.hp.temp;
- data.attributes.shld.max = data.attributes.hp.tempmax;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare skill checks.
- * @param actorData
- * @param bonuses Global bonus data.
- * @param checkBonus Ability check specific bonus.
- * @param originalSkills A transformed actor's original actor's skills.
- * @private
- */
- _prepareSkills(actorData, bonuses, checkBonus, originalSkills) {
- if (actorData.type === 'vehicle') return;
-
- const data = actorData.data;
- const flags = actorData.flags.sw5e || {};
-
- // Skill modifiers
- const feats = SW5E.characterFlags;
- const athlete = flags.remarkableAthlete;
- const joat = flags.jackOfAllTrades;
- const observant = flags.observantFeat;
- const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0;
- for (let [id, skl] of Object.entries(data.skills)) {
- skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0;
- let round = Math.floor;
-
- // Remarkable
- if ( athlete && (skl.value < 0.5) && feats.remarkableAthlete.abilities.includes(skl.ability) ) {
- skl.value = 0.5;
- round = Math.ceil;
- }
-
- // Jack of All Trades
- if ( joat && (skl.value < 0.5) ) {
- skl.value = 0.5;
- }
-
- // Polymorph Skill Proficiencies
- 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(skl.value * data.attributes.prof);
- skl.total = skl.mod + skl.prof + skl.bonus;
-
- // Compute passive bonus
- const passive = observant && (feats.observantFeat.skills.includes(id)) ? 5 : 0;
- skl.passive = 10 + skl.total + passive;
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare data related to the power-casting capabilities of the Actor
- * @private
- */
- _computeBasePowercasting (actorData) {
- if (actorData.type === 'vehicle' || actorData.type === 'starship') return;
- const ad = actorData.data;
- const powers = ad.powers;
- const isNPC = actorData.type === 'npc';
-
- // Powercasting DC
- // TODO: Consider an option for using the variant rule of all powers use the same value
- ad.attributes.powerForceLightDC = 8 + ad.abilities.wis.mod + ad.attributes.prof ?? 10;
- ad.attributes.powerForceDarkDC = 8 + ad.abilities.cha.mod + ad.attributes.prof ?? 10;
- ad.attributes.powerForceUnivDC = Math.max(ad.attributes.powerForceLightDC,ad.attributes.powerForceDarkDC) ?? 10
- ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10;
-
- // Translate the list of classes into force and tech power-casting progression
- const forceProgression = {
- classes: 0,
- levels: 0,
- multi: 0,
- maxClass: "none",
- maxClassPriority: 0,
- maxClassLevels: 0,
- maxClassPowerLevel: 0,
- powersKnown: 0,
- points: 0
- };
- const techProgression = {
- classes: 0,
- levels: 0,
- multi: 0,
- maxClass: "none",
- maxClassPriority: 0,
- maxClassLevels: 0,
- maxClassPowerLevel: 0,
- powersKnown: 0,
- points: 0
- };
-
- // Tabulate the total power-casting progression
- const classes = this.data.items.filter(i => i.type === "class");
- let priority = 0;
- for ( let cls of classes ) {
- const d = cls.data.data;
- if ( d.powercasting.progression === "none" ) continue;
- const levels = d.levels;
- const prog = d.powercasting.progression;
- // TODO: Consider a more dynamic system
- switch (prog) {
- case 'consular':
- priority = 3;
- forceProgression.levels += levels;
- forceProgression.multi += (SW5E.powerMaxLevel['consular'][19]/9)*levels;
- forceProgression.classes++;
- // see if class controls high level forcecasting
- if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
- forceProgression.maxClass = 'consular';
- forceProgression.maxClassLevels = levels;
- forceProgression.maxClassPriority = priority;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['consular'][Math.clamped((levels - 1), 0, 20)];
- }
- // calculate points and powers known
- forceProgression.powersKnown += SW5E.powersKnown['consular'][Math.clamped((levels - 1), 0, 20)];
- forceProgression.points += SW5E.powerPoints['consular'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'engineer':
- priority = 2
- techProgression.levels += levels;
- techProgression.multi += (SW5E.powerMaxLevel['engineer'][19]/9)*levels;
- techProgression.classes++;
- // see if class controls high level techcasting
- if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){
- techProgression.maxClass = 'engineer';
- techProgression.maxClassLevels = levels;
- techProgression.maxClassPriority = priority;
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['engineer'][Math.clamped((levels - 1), 0, 20)];
- }
- techProgression.powersKnown += SW5E.powersKnown['engineer'][Math.clamped((levels - 1), 0, 20)];
- techProgression.points += SW5E.powerPoints['engineer'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'guardian':
- priority = 1;
- forceProgression.levels += levels;
- forceProgression.multi += (SW5E.powerMaxLevel['guardian'][19]/9)*levels;
- forceProgression.classes++;
- // see if class controls high level forcecasting
- if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
- forceProgression.maxClass = 'guardian';
- forceProgression.maxClassLevels = levels;
- forceProgression.maxClassPriority = priority;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['guardian'][Math.clamped((levels - 1), 0, 20)];
- }
- forceProgression.powersKnown += SW5E.powersKnown['guardian'][Math.clamped((levels - 1), 0, 20)];
- forceProgression.points += SW5E.powerPoints['guardian'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'scout':
- priority = 1;
- techProgression.levels += levels;
- techProgression.multi += (SW5E.powerMaxLevel['scout'][19]/9)*levels;
- techProgression.classes++;
- // see if class controls high level techcasting
- if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){
- techProgression.maxClass = 'scout';
- techProgression.maxClassLevels = levels;
- techProgression.maxClassPriority = priority;
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['scout'][Math.clamped((levels - 1), 0, 20)];
- }
- techProgression.powersKnown += SW5E.powersKnown['scout'][Math.clamped((levels - 1), 0, 20)];
- techProgression.points += SW5E.powerPoints['scout'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'sentinel':
- priority = 2;
- forceProgression.levels += levels;
- forceProgression.multi += (SW5E.powerMaxLevel['sentinel'][19]/9)*levels;
- forceProgression.classes++;
- // see if class controls high level forcecasting
- if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
- forceProgression.maxClass = 'sentinel';
- forceProgression.maxClassLevels = levels;
- forceProgression.maxClassPriority = priority;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['sentinel'][Math.clamped((levels - 1), 0, 20)];
- }
- forceProgression.powersKnown += SW5E.powersKnown['sentinel'][Math.clamped((levels - 1), 0, 20)];
- forceProgression.points += SW5E.powerPoints['sentinel'][Math.clamped((levels - 1), 0, 20)];
- break; }
- }
-
- if (isNPC) {
- // EXCEPTION: NPC with an explicit power-caster level
- if (ad.details.powerForceLevel) {
- forceProgression.levels = ad.details.powerForceLevel;
- ad.attributes.force.level = forceProgression.levels;
- forceProgression.maxClass = ad.attributes.powercasting;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped((forceProgression.levels - 1), 0, 20)];
- }
- if (ad.details.powerTechLevel) {
- techProgression.levels = ad.details.powerTechLevel;
- ad.attributes.tech.level = techProgression.levels;
- techProgression.maxClass = ad.attributes.powercasting;
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped((techProgression.levels - 1), 0, 20)];
- }
- } else {
- // EXCEPTION: multi-classed progression uses multi rounded down rather than levels
- if (forceProgression.classes > 1) {
- forceProgression.levels = Math.floor(forceProgression.multi);
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][forceProgression.levels - 1];
- }
- if (techProgression.classes > 1) {
- techProgression.levels = Math.floor(techProgression.multi);
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][techProgression.levels - 1];
- }
- }
-
-
- // Look up the number of slots per level from the powerLimit table
- let forcePowerLimit = Array.from(SW5E.powerLimit['none']);
- for (let i = 0; i < (forceProgression.maxClassPowerLevel); i++) {
- forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i];
- }
-
- for ( let [n, lvl] of Object.entries(powers) ) {
- let i = parseInt(n.slice(-1));
- if ( Number.isNaN(i) ) continue;
- if ( Number.isNumeric(lvl.foverride) ) lvl.fmax = Math.max(parseInt(lvl.foverride), 0);
- else lvl.fmax = forcePowerLimit[i-1] || 0;
- if (isNPC){
- lvl.fvalue = lvl.fmax;
- }else{
- lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax),lvl.fmax);
- }
- }
-
- let techPowerLimit = Array.from(SW5E.powerLimit['none']);
- for (let i = 0; i < (techProgression.maxClassPowerLevel); i++) {
- techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i];
- }
-
- for ( let [n, lvl] of Object.entries(powers) ) {
- let i = parseInt(n.slice(-1));
- if ( Number.isNaN(i) ) continue;
- if ( Number.isNumeric(lvl.toverride) ) lvl.tmax = Math.max(parseInt(lvl.toverride), 0);
- else lvl.tmax = techPowerLimit[i-1] || 0;
- if (isNPC){
- lvl.tvalue = lvl.tmax;
- }else{
- lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax),lvl.tmax);
- }
- }
-
- // Set Force and tech power for PC Actors
- if (!isNPC) {
- if (forceProgression.levels) {
- ad.attributes.force.known.max = forceProgression.powersKnown;
- ad.attributes.force.points.max = forceProgression.points + Math.max(ad.abilities.wis.mod, ad.abilities.cha.mod);
- ad.attributes.force.level = forceProgression.levels;
- }
- if (techProgression.levels){
- ad.attributes.tech.known.max = techProgression.powersKnown;
- ad.attributes.tech.points.max = techProgression.points + ad.abilities.int.mod;
- ad.attributes.tech.level = techProgression.levels;
- }
- }
-
-
- // Tally Powers Known and check for migration first to avoid errors
- let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined;
- if ( hasKnownPowers ) {
- const knownPowers = this.data.items.filter(i => i.type === "power");
- let knownForcePowers = 0;
- let knownTechPowers = 0;
- for ( let knownPower of knownPowers ) {
- const d = knownPower.data;
- switch (d.data.school){
- case "lgt":
- case "uni":
- case "drk":{
- knownForcePowers++;
- break;
- }
- case "tec":{
- knownTechPowers++;
- break;
- }
+ // 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)) {
+ abl.mod = Math.floor((abl.value - 10) / 2);
+ abl.prof = (abl.proficient || 0) * data.attributes.prof;
+ abl.saveBonus = saveBonus;
+ abl.checkBonus = checkBonus;
+ abl.save = abl.mod + abl.prof + abl.saveBonus;
+ abl.dc = 8 + abl.mod + data.attributes.prof + dcBonus;
+
+ // If we merged saves when transforming, take the highest bonus here.
+ if (originalSaves && abl.proficient) {
+ abl.save = Math.max(abl.save, originalSaves[id].save);
+ }
}
- }
- ad.attributes.force.known.value = knownForcePowers;
- ad.attributes.tech.known.value = knownTechPowers;
- }
- }
- /* -------------------------------------------- */
+ // Inventory encumbrance
+ data.attributes.encumbrance = this._computeEncumbrance(actorData);
- /**
- * Prepare data related to the power-casting capabilities of the Actor
- * @private
- */
- _computeDerivedPowercasting (actorData) {
+ // Prepare skills
+ this._prepareSkills(actorData, bonuses, checkBonus, originalSkills);
- if ((actorData.type === 'actor') || (actorData.type === 'npc')) return;
-
- const ad = actorData.data;
+ // Reset class store to ensure it is updated with any changes
+ this._classes = undefined;
- // Powercasting DC for Actors and NPCs
- // TODO: Consider an option for using the variant rule of all powers use the same value
- ad.attributes.powerForceLightDC = 8 + ad.abilities.wis.mod + ad.attributes.prof ?? 10;
- ad.attributes.powerForceDarkDC = 8 + ad.abilities.cha.mod + ad.attributes.prof ?? 10;
- ad.attributes.powerForceUnivDC = Math.max(ad.attributes.powerForceLightDC,ad.attributes.powerForceDarkDC) ?? 10
- ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10;
+ // Determine Initiative Modifier
+ const init = data.attributes.init;
+ const athlete = flags.remarkableAthlete;
+ const joat = flags.jackOfAllTrades;
+ init.mod = data.abilities.dex.mod;
+ 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.value = init.value ?? 0;
+ init.bonus = init.value + (flags.initiativeAlert ? 5 : 0);
+ init.total = init.mod + init.prof + init.bonus;
- if (actorData.type !== 'actor') return;
-
- // Set Force and tech bonus points for PC Actors
- if (!!ad.attributes.force.level){
- ad.attributes.force.points.max += Math.max(ad.abilities.wis.mod,ad.abilities.cha.mod);
- }
- if (!!ad.attributes.tech.level){
- ad.attributes.tech.points.max += ad.abilities.int.mod;
- }
-
- }
-
- /* -------------------------------------------- */
-
- /**
- * Compute the level and percentage of encumbrance for an Actor.
- *
- * Optionally include the weight of carried currency across all denominations by applying the standard rule
- * from the PHB pg. 143
- * @param {Object} actorData The data object for the Actor being rendered
- * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level
- * @private
- */
- _computeEncumbrance(actorData) {
- // TODO: Maybe add an option for variant encumbrance
- // Get the total weight from items
- const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"];
- let weight = actorData.items.reduce((weight, i) => {
- if ( !physicalItems.includes(i.type) ) return weight;
- const q = i.data.data.quantity || 0;
- const w = i.data.data.weight || 0;
- return weight + (q * w);
- }, 0);
-
- // [Optional] add Currency Weight (for non-transformed actors)
- if ( game.settings.get("sw5e", "currencyWeight") && actorData.data.currency ) {
- const currency = actorData.data.currency;
- const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0);
- weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
- }
-
- // Determine the encumbrance size class
- let mod = {
- tiny: 0.5,
- sm: 1,
- med: 1,
- lg: 2,
- huge: 4,
- grg: 8
- }[actorData.data.traits.size] || 1;
- 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.toNearest(0.1), max, pct, encumbered: pct > (2/3) };
- }
-
- /* -------------------------------------------- */
- /* Event Handlers */
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- async _preCreate(data, options, user) {
- await super._preCreate(data, options, user);
-
- // Token size category
- const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"];
- this.data.token.update({width: s, height: s});
-
- // Player character configuration
- if ( this.type === "character" ) {
- this.data.token.update({vision: true, actorLink: true, disposition: 1});
- }
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- async _preUpdate(changed, options, user) {
- await super._preUpdate(changed, options, user);
-
- // Apply changes in Actor size to Token width/height
- const newSize = foundry.utils.getProperty(changed, "data.traits.size");
- if ( newSize && (newSize !== foundry.utils.getProperty(this.data, "data.traits.size")) ) {
- let size = CONFIG.SW5E.tokenSizes[newSize];
- if ( !foundry.utils.hasProperty(changed, "token.width") ) {
- changed.token = changed.token || {};
- changed.token.height = size;
- changed.token.width = size;
- }
- }
-
- // Reset death save counters
- const isDead = this.data.data.attributes.hp.value <= 0;
- if ( isDead && (foundry.utils.getProperty(changed, "data.attributes.hp.value") > 0) ) {
- foundry.utils.setProperty(changed, "data.attributes.death.success", 0);
- foundry.utils.setProperty(changed, "data.attributes.death.failure", 0);
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Assign a class item as the original class for the Actor based on which class has the most levels
- * @protected
- */
- _assignPrimaryClass() {
- const classes = this.itemTypes.class.sort((a, b) => b.data.data.levels - a.data.data.levels);
- const newPC = classes[0]?.id || "";
- return this.update({"data.details.originalClass": newPC});
- }
-
- /* -------------------------------------------- */
- /* Gameplay Mechanics */
- /* -------------------------------------------- */
-
- /** @override */
- async modifyTokenAttribute(attribute, value, isDelta, isBar) {
- if ( attribute === "attributes.hp" ) {
- const hp = getProperty(this.data.data, attribute);
- const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value;
- return this.applyDamage(delta);
- }
- return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Apply a certain amount of damage or healing to the health pool for Actor
- * @param {number} amount An amount of damage (positive) or healing (negative) to sustain
- * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing
- * @return {Promise} A Promise which resolves once the damage has been applied
- */
- async applyDamage(amount=0, multiplier=1) {
- amount = Math.floor(parseInt(amount) * multiplier);
- const hp = this.data.data.attributes.hp;
-
- // Deduct damage from temp HP first
- const tmp = parseInt(hp.temp) || 0;
- const dt = amount > 0 ? Math.min(tmp, amount) : 0;
-
- // Remaining goes to health
- const tmpMax = parseInt(hp.tempmax) || 0;
- const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax);
-
- // Update the Actor
- const updates = {
- "data.attributes.hp.temp": tmp - dt,
- "data.attributes.hp.value": dh
- };
-
- // Delegate damage application to a hook
- // TODO replace this in the future with a better modifyTokenAttribute function in the core
- const allowed = Hooks.call("modifyTokenAttribute", {
- attribute: "attributes.hp",
- value: amount,
- isDelta: false,
- isBar: true
- }, updates);
- return allowed !== false ? this.update(updates) : this;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll a Skill Check
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {string} skillId The skill id (e.g. "ins")
- * @param {Object} options Options which configure how the skill check is rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollSkill(skillId, options={}) {
- const skl = this.data.data.skills[skillId];
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
-
- // Compose roll parts and data
- const parts = ["@mod"];
- const data = {mod: skl.mod + skl.prof};
-
- // Ability test bonus
- if ( bonuses.check ) {
- data["checkBonus"] = bonuses.check;
- parts.push("@checkBonus");
- }
-
- // Skill check bonus
- if ( bonuses.skill ) {
- data["skillBonus"] = bonuses.skill;
- parts.push("@skillBonus");
- }
-
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
- }
-
- // Reliable Talent applies to any skill check we have full or better proficiency in
- const reliableTalent = (skl.value >= 1 && this.getFlag("sw5e", "reliableTalent"));
-
- // Roll and return
- const rollData = foundry.utils.mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.format("SW5E.SkillPromptTitle", {skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]}),
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- reliableTalent: reliableTalent,
- messageData: {
- speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
- "flags.sw5e.roll": {type: "skill", skillId }
- }
- });
- return d20Roll(rollData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll a generic ability test or saving throw.
- * Prompt the user for input on which variety of roll they want to do.
- * @param {String}abilityId The ability id (e.g. "str")
- * @param {Object} options Options which configure how ability tests or saving throws are rolled
- */
- rollAbility(abilityId, options={}) {
- const label = CONFIG.SW5E.abilities[abilityId];
- new Dialog({
- title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
- content: `${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}
`,
- buttons: {
- test: {
- label: game.i18n.localize("SW5E.ActionAbil"),
- callback: () => this.rollAbilityTest(abilityId, options)
- },
- save: {
- label: game.i18n.localize("SW5E.ActionSave"),
- callback: () => this.rollAbilitySave(abilityId, options)
+ // Cache labels
+ this.labels = {};
+ if (this.type === "npc") {
+ this.labels["creatureType"] = this.constructor.formatCreatureType(data.details.type);
}
- }
- }).render(true);
- }
- /* -------------------------------------------- */
-
- /**
- * Roll an Ability Test
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {String} abilityId The ability ID (e.g. "str")
- * @param {Object} options Options which configure how ability tests are rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollAbilityTest(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};
-
- // Add feat-related proficiency bonuses
- const feats = this.data.flags.sw5e || {};
- if ( feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId) ) {
- parts.push("@proficiency");
- data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof);
- }
- else if ( feats.jackOfAllTrades ) {
- parts.push("@proficiency");
- data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof);
+ // Prepare power-casting data
+ this._computeDerivedPowercasting(this.data);
}
- // Add global actor bonus
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
- if ( bonuses.check ) {
- parts.push("@checkBonus");
- data.checkBonus = bonuses.check;
+ /* -------------------------------------------- */
+
+ /**
+ * Return the amount of experience required to gain a certain character level.
+ * @param level {Number} The desired level
+ * @return {Number} The XP required
+ */
+ getLevelExp(level) {
+ const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS;
+ return levels[Math.min(level, levels.length - 1)];
}
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
+ /* -------------------------------------------- */
+
+ /**
+ * Return the amount of experience granted by killing a creature of a certain CR.
+ * @param cr {Number} The creature's challenge rating
+ * @return {Number} The amount of experience granted per kill
+ */
+ getCRExp(cr) {
+ if (cr < 1.0) return Math.max(200 * cr, 10);
+ return CONFIG.SW5E.CR_EXP_LEVELS[cr];
}
- // Roll and return
- const rollData = foundry.utils.mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
- halflingLucky: feats.halflingLucky,
- messageData: {
- speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
- "flags.sw5e.roll": {type: "ability", abilityId }
- }
- });
- return d20Roll(rollData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
-
- /**
- * Roll an Ability Saving Throw
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {String} abilityId The ability ID (e.g. "str")
- * @param {Object} options Options which configure how ability tests are rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- 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};
-
- // Include proficiency bonus
- if ( abl.prof > 0 ) {
- parts.push("@prof");
- data.prof = abl.prof;
+ /** @inheritdoc */
+ getRollData() {
+ const data = super.getRollData();
+ data.prof = this.data.data.attributes.prof || 0;
+ data.classes = Object.entries(this.classes).reduce((obj, e) => {
+ const [slug, cls] = e;
+ obj[slug] = cls.data.data;
+ return obj;
+ }, {});
+ return data;
}
- // Include a global actor ability save bonus
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
- if ( bonuses.save ) {
- parts.push("@saveBonus");
- data.saveBonus = bonuses.save;
+ /* -------------------------------------------- */
+
+ /**
+ * Given a list of items to add to the Actor, optionally prompt the
+ * user for which they would like to add.
+ * @param {Array.} items - The items being added to the Actor.
+ * @param {boolean} [prompt=true] - Whether or not to prompt the user.
+ * @returns {Promise}
+ */
+ async addEmbeddedItems(items, prompt = true) {
+ let itemsToAdd = items;
+ if (!items.length) return [];
+
+ // Obtain the array of item creation data
+ let toCreate = [];
+ if (prompt) {
+ const itemIdsToAdd = await SelectItemsPrompt.create(items, {
+ hint: game.i18n.localize("SW5E.AddEmbeddedItemPromptHint")
+ });
+ for (let item of items) {
+ if (itemIdsToAdd.includes(item.id)) toCreate.push(item.toObject());
+ }
+ } else {
+ toCreate = items.map((item) => item.toObject());
+ }
+
+ // Create the requested items
+ if (itemsToAdd.length === 0) return [];
+ return Item5e.createDocuments(toCreate, {parent: this});
}
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
+ /* -------------------------------------------- */
+
+ /**
+ * Get a list of features to add to the Actor when a class item is updated.
+ * Optionally prompt the user for which they would like to add.
+ */
+ async getClassFeatures({className, archetypeName, level} = {}) {
+ const existing = new Set(this.items.map((i) => i.name));
+ const features = await Actor5e.loadClassFeatures({className, archetypeName, level});
+ return features.filter((f) => !existing.has(f.name)) || [];
}
- // Roll and return
- const rollData = foundry.utils.mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}),
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- messageData: {
- speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
- "flags.sw5e.roll": {type: "save", abilityId }
- }
- });
- return d20Roll(rollData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Return the features which a character is awarded for each class level
+ * @param {string} className The class name being added
+ * @param {string} archetypeName The archetype 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} Array of Item5e entities
+ */
+ static async loadClassFeatures({className = "", archetypeName = "", level = 1, priorLevel = 0} = {}) {
+ className = className.toLowerCase();
+ archetypeName = archetypeName.slugify();
- /**
- * Perform a death saving throw, rolling a d20 plus any global save bonuses
- * @param {Object} options Additional options which modify the roll
- * @return {Promise} A Promise which resolves to the Roll instance
- */
- async rollDeathSave(options={}) {
+ // Get the configuration of features which may be added
+ const clsConfig = CONFIG.SW5E.classFeatures[className];
+ if (!clsConfig) return [];
- // Display a warning if we are not at zero HP or if we already have reached 3
- const death = this.data.data.attributes.death;
- if ( (this.data.data.attributes.hp.value > 0) || (death.failure >= 3) || (death.success >= 3)) {
- ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary"));
- return null;
+ // 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 archetype features
+ const archConfig = clsConfig.archetypes[archetypeName] || {};
+ for (let [l, f] of Object.entries(archConfig.features || {})) {
+ l = parseInt(l);
+ if (l <= level && l > priorLevel) ids = ids.concat(f);
+ }
+
+ // Load item data for all identified features
+ const features = [];
+ for (let id of ids) {
+ features.push(await 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;
}
- // Evaluate a global saving throw bonus
- const parts = [];
- const data = {};
+ /* -------------------------------------------- */
+ /* Data Preparation Helpers */
+ /* -------------------------------------------- */
- // Include a global actor ability save bonus
- const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {};
- if ( bonuses.save ) {
- parts.push("@saveBonus");
- data.saveBonus = bonuses.save;
+ /**
+ * Prepare Character type specific data
+ */
+ _prepareCharacterData(actorData) {
+ const data = actorData.data;
+
+ // Determine character level and available hit dice based on owned Class items
+ const [level, hd] = this.items.reduce(
+ (arr, item) => {
+ if (item.type === "class") {
+ const classLevels = parseInt(item.data.data.levels) || 1;
+ arr[0] += classLevels;
+ arr[1] += classLevels - (parseInt(item.data.data.hitDiceUsed) || 0);
+ }
+ return arr;
+ },
+ [0, 0]
+ );
+ data.details.level = level;
+ data.attributes.hd = hd;
+
+ // Character proficiency bonus
+ data.attributes.prof = Math.floor((level + 7) / 4);
+
+ // Experience required for next level
+ const xp = data.details.xp;
+ xp.max = this.getLevelExp(level || 1);
+ const prior = this.getLevelExp(level - 1 || 0);
+ const required = xp.max - prior;
+ const pct = Math.round(((xp.value - prior) * 100) / required);
+ xp.pct = Math.clamped(pct, 0, 100);
+
+ // Add base Powercasting attributes
+ this._computeBasePowercasting(actorData);
}
- // Evaluate the roll
- const rollData = foundry.utils.mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.localize("SW5E.DeathSavingThrow"),
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- targetValue: 10,
- messageData: {
- speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
- "flags.sw5e.roll": {type: "death"}
- }
- });
- const roll = await d20Roll(rollData);
- if ( !roll ) return null;
+ /* -------------------------------------------- */
- // Take action depending on the result
- const success = roll.total >= 10;
- const d20 = roll.dice[0].total;
+ /**
+ * Prepare NPC type specific data
+ */
+ _prepareNPCData(actorData) {
+ const data = actorData.data;
- let chatString;
+ // Kill Experience
+ data.details.xp.value = this.getCRExp(data.details.cr);
- // Save success
- if ( success ) {
- let successes = (death.success || 0) + 1;
+ // Proficiency
+ data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4);
- // Critical Success = revive with 1hp
- if ( d20 === 20 ) {
- await this.update({
- "data.attributes.death.success": 0,
- "data.attributes.death.failure": 0,
- "data.attributes.hp.value": 1
+ this._computeBasePowercasting(actorData);
+
+ // Powercaster Level
+ if (data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel)) {
+ data.details.powerLevel = Math.max(data.details.cr, 1);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare vehicle type-specific data
+ * @param actorData
+ * @private
+ */
+ _prepareVehicleData(actorData) {}
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare starship type-specific data
+ * @param actorData
+ * @private
+ */
+ _prepareStarshipData(actorData) {
+ const data = actorData.data;
+
+ // Proficiency
+ data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4);
+
+ // Link hull to hp and shields to temp hp
+ data.attributes.hull.value = data.attributes.hp.value;
+ data.attributes.hull.max = data.attributes.hp.max;
+ data.attributes.shld.value = data.attributes.hp.temp;
+ data.attributes.shld.max = data.attributes.hp.tempmax;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare skill checks.
+ * @param actorData
+ * @param bonuses Global bonus data.
+ * @param checkBonus Ability check specific bonus.
+ * @param originalSkills A transformed actor's original actor's skills.
+ * @private
+ */
+ _prepareSkills(actorData, bonuses, checkBonus, originalSkills) {
+ if (actorData.type === "vehicle") return;
+
+ const data = actorData.data;
+ const flags = actorData.flags.sw5e || {};
+
+ // Skill modifiers
+ const feats = SW5E.characterFlags;
+ const athlete = flags.remarkableAthlete;
+ const joat = flags.jackOfAllTrades;
+ const observant = flags.observantFeat;
+ const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0;
+ for (let [id, skl] of Object.entries(data.skills)) {
+ skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0;
+ let round = Math.floor;
+
+ // Remarkable
+ if (athlete && skl.value < 0.5 && feats.remarkableAthlete.abilities.includes(skl.ability)) {
+ skl.value = 0.5;
+ round = Math.ceil;
+ }
+
+ // Jack of All Trades
+ if (joat && skl.value < 0.5) {
+ skl.value = 0.5;
+ }
+
+ // Polymorph Skill Proficiencies
+ 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(skl.value * data.attributes.prof);
+ skl.total = skl.mod + skl.prof + skl.bonus;
+
+ // Compute passive bonus
+ const passive = observant && feats.observantFeat.skills.includes(id) ? 5 : 0;
+ skl.passive = 10 + skl.total + passive;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare data related to the power-casting capabilities of the Actor
+ * @private
+ */
+ _computeBasePowercasting(actorData) {
+ if (actorData.type === "vehicle" || actorData.type === "starship") return;
+ const ad = actorData.data;
+ const powers = ad.powers;
+ const isNPC = actorData.type === "npc";
+
+ // Translate the list of classes into force and tech power-casting progression
+ const forceProgression = {
+ classes: 0,
+ levels: 0,
+ multi: 0,
+ maxClass: "none",
+ maxClassPriority: 0,
+ maxClassLevels: 0,
+ maxClassPowerLevel: 0,
+ powersKnown: 0,
+ points: 0
+ };
+ const techProgression = {
+ classes: 0,
+ levels: 0,
+ multi: 0,
+ maxClass: "none",
+ maxClassPriority: 0,
+ maxClassLevels: 0,
+ maxClassPowerLevel: 0,
+ powersKnown: 0,
+ points: 0
+ };
+
+ // Tabulate the total power-casting progression
+ const classes = this.data.items.filter((i) => i.type === "class");
+ let priority = 0;
+ for (let cls of classes) {
+ const d = cls.data.data;
+ if (d.powercasting.progression === "none") continue;
+ const levels = d.levels;
+ const prog = d.powercasting.progression;
+ // TODO: Consider a more dynamic system
+ switch (prog) {
+ case "consular":
+ priority = 3;
+ forceProgression.levels += levels;
+ forceProgression.multi += (SW5E.powerMaxLevel["consular"][19] / 9) * levels;
+ forceProgression.classes++;
+ // see if class controls high level forcecasting
+ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
+ forceProgression.maxClass = "consular";
+ forceProgression.maxClassLevels = levels;
+ forceProgression.maxClassPriority = priority;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["consular"][Math.clamped(levels - 1, 0, 20)];
+ }
+ // calculate points and powers known
+ forceProgression.powersKnown += SW5E.powersKnown["consular"][Math.clamped(levels - 1, 0, 20)];
+ forceProgression.points += SW5E.powerPoints["consular"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "engineer":
+ priority = 2;
+ techProgression.levels += levels;
+ techProgression.multi += (SW5E.powerMaxLevel["engineer"][19] / 9) * levels;
+ techProgression.classes++;
+ // see if class controls high level techcasting
+ if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) {
+ techProgression.maxClass = "engineer";
+ techProgression.maxClassLevels = levels;
+ techProgression.maxClassPriority = priority;
+ techProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["engineer"][Math.clamped(levels - 1, 0, 20)];
+ }
+ techProgression.powersKnown += SW5E.powersKnown["engineer"][Math.clamped(levels - 1, 0, 20)];
+ techProgression.points += SW5E.powerPoints["engineer"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "guardian":
+ priority = 1;
+ forceProgression.levels += levels;
+ forceProgression.multi += (SW5E.powerMaxLevel["guardian"][19] / 9) * levels;
+ forceProgression.classes++;
+ // see if class controls high level forcecasting
+ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
+ forceProgression.maxClass = "guardian";
+ forceProgression.maxClassLevels = levels;
+ forceProgression.maxClassPriority = priority;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["guardian"][Math.clamped(levels - 1, 0, 20)];
+ }
+ forceProgression.powersKnown += SW5E.powersKnown["guardian"][Math.clamped(levels - 1, 0, 20)];
+ forceProgression.points += SW5E.powerPoints["guardian"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "scout":
+ priority = 1;
+ techProgression.levels += levels;
+ techProgression.multi += (SW5E.powerMaxLevel["scout"][19] / 9) * levels;
+ techProgression.classes++;
+ // see if class controls high level techcasting
+ if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) {
+ techProgression.maxClass = "scout";
+ techProgression.maxClassLevels = levels;
+ techProgression.maxClassPriority = priority;
+ techProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["scout"][Math.clamped(levels - 1, 0, 20)];
+ }
+ techProgression.powersKnown += SW5E.powersKnown["scout"][Math.clamped(levels - 1, 0, 20)];
+ techProgression.points += SW5E.powerPoints["scout"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "sentinel":
+ priority = 2;
+ forceProgression.levels += levels;
+ forceProgression.multi += (SW5E.powerMaxLevel["sentinel"][19] / 9) * levels;
+ forceProgression.classes++;
+ // see if class controls high level forcecasting
+ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
+ forceProgression.maxClass = "sentinel";
+ forceProgression.maxClassLevels = levels;
+ forceProgression.maxClassPriority = priority;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["sentinel"][Math.clamped(levels - 1, 0, 20)];
+ }
+ forceProgression.powersKnown += SW5E.powersKnown["sentinel"][Math.clamped(levels - 1, 0, 20)];
+ forceProgression.points += SW5E.powerPoints["sentinel"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ }
+ }
+
+ if (isNPC) {
+ // EXCEPTION: NPC with an explicit power-caster level
+ if (ad.details.powerForceLevel) {
+ forceProgression.levels = ad.details.powerForceLevel;
+ ad.attributes.force.level = forceProgression.levels;
+ forceProgression.maxClass = ad.attributes.powercasting;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped(forceProgression.levels - 1, 0, 20)];
+ }
+ if (ad.details.powerTechLevel) {
+ techProgression.levels = ad.details.powerTechLevel;
+ ad.attributes.tech.level = techProgression.levels;
+ techProgression.maxClass = ad.attributes.powercasting;
+ techProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped(techProgression.levels - 1, 0, 20)];
+ }
+ } else {
+ // EXCEPTION: multi-classed progression uses multi rounded down rather than levels
+ if (forceProgression.classes > 1) {
+ forceProgression.levels = Math.floor(forceProgression.multi);
+ forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][forceProgression.levels - 1];
+ }
+ if (techProgression.classes > 1) {
+ techProgression.levels = Math.floor(techProgression.multi);
+ techProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][techProgression.levels - 1];
+ }
+ }
+
+ // Look up the number of slots per level from the powerLimit table
+ let forcePowerLimit = Array.from(SW5E.powerLimit["none"]);
+ for (let i = 0; i < forceProgression.maxClassPowerLevel; i++) {
+ forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i];
+ }
+
+ for (let [n, lvl] of Object.entries(powers)) {
+ let i = parseInt(n.slice(-1));
+ if (Number.isNaN(i)) continue;
+ if (Number.isNumeric(lvl.foverride)) lvl.fmax = Math.max(parseInt(lvl.foverride), 0);
+ else lvl.fmax = forcePowerLimit[i - 1] || 0;
+ if (isNPC) {
+ lvl.fvalue = lvl.fmax;
+ } else {
+ lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax), lvl.fmax);
+ }
+ }
+
+ let techPowerLimit = Array.from(SW5E.powerLimit["none"]);
+ for (let i = 0; i < techProgression.maxClassPowerLevel; i++) {
+ techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i];
+ }
+
+ for (let [n, lvl] of Object.entries(powers)) {
+ let i = parseInt(n.slice(-1));
+ if (Number.isNaN(i)) continue;
+ if (Number.isNumeric(lvl.toverride)) lvl.tmax = Math.max(parseInt(lvl.toverride), 0);
+ else lvl.tmax = techPowerLimit[i - 1] || 0;
+ if (isNPC) {
+ lvl.tvalue = lvl.tmax;
+ } else {
+ lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax), lvl.tmax);
+ }
+ }
+
+ // Set Force and tech power for PC Actors
+ if (!isNPC) {
+ if (forceProgression.levels) {
+ ad.attributes.force.known.max = forceProgression.powersKnown;
+ ad.attributes.force.points.max = forceProgression.points;
+ ad.attributes.force.level = forceProgression.levels;
+ }
+ if (techProgression.levels) {
+ ad.attributes.tech.known.max = techProgression.powersKnown;
+ ad.attributes.tech.points.max = techProgression.points;
+ ad.attributes.tech.level = techProgression.levels;
+ }
+ }
+
+ // Tally Powers Known and check for migration first to avoid errors
+ let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined;
+ if (hasKnownPowers) {
+ const knownPowers = this.data.items.filter((i) => i.type === "power");
+ let knownForcePowers = 0;
+ let knownTechPowers = 0;
+ for (let knownPower of knownPowers) {
+ const d = knownPower.data;
+ switch (d.data.school) {
+ case "lgt":
+ case "uni":
+ case "drk": {
+ knownForcePowers++;
+ break;
+ }
+ case "tec": {
+ knownTechPowers++;
+ break;
+ }
+ }
+ }
+ ad.attributes.force.known.value = knownForcePowers;
+ ad.attributes.tech.known.value = knownTechPowers;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare data related to the power-casting capabilities of the Actor
+ * @private
+ */
+ _computeDerivedPowercasting(actorData) {
+ if (!(actorData.type === "character" || actorData.type === "npc")) return;
+
+ const ad = actorData.data;
+
+ // Powercasting DC for Actors and NPCs
+ // TODO: Consider an option for using the variant rule of all powers use the same value
+ ad.attributes.powerForceLightDC = 8 + ad.abilities.wis.mod + ad.attributes.prof ?? 10;
+ ad.attributes.powerForceDarkDC = 8 + ad.abilities.cha.mod + ad.attributes.prof ?? 10;
+ ad.attributes.powerForceUnivDC =
+ Math.max(ad.attributes.powerForceLightDC, ad.attributes.powerForceDarkDC) ?? 10;
+ ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10;
+
+ if (actorData.type !== "character") return;
+
+ // Set Force and tech bonus points for PC Actors
+ if (!!ad.attributes.force.level) {
+ ad.attributes.force.points.max += Math.max(ad.abilities.wis.mod, ad.abilities.cha.mod);
+ }
+ if (!!ad.attributes.tech.level) {
+ ad.attributes.tech.points.max += ad.abilities.int.mod;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Compute the level and percentage of encumbrance for an Actor.
+ *
+ * Optionally include the weight of carried currency across all denominations by applying the standard rule
+ * from the PHB pg. 143
+ * @param {Object} actorData The data object for the Actor being rendered
+ * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level
+ * @private
+ */
+ _computeEncumbrance(actorData) {
+ // TODO: Maybe add an option for variant encumbrance
+ // Get the total weight from items
+ const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"];
+ let weight = actorData.items.reduce((weight, i) => {
+ if (!physicalItems.includes(i.type)) return weight;
+ const q = i.data.data.quantity || 0;
+ const w = i.data.data.weight || 0;
+ return weight + q * w;
+ }, 0);
+
+ // [Optional] add Currency Weight (for non-transformed actors)
+ if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) {
+ const currency = actorData.data.currency;
+ const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0);
+ weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
+ }
+
+ // Determine the encumbrance size class
+ let mod =
+ {
+ tiny: 0.5,
+ sm: 1,
+ med: 1,
+ lg: 2,
+ huge: 4,
+ grg: 8
+ }[actorData.data.traits.size] || 1;
+ 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.toNearest(0.1), max, pct, encumbered: pct > 2 / 3};
+ }
+
+ /* -------------------------------------------- */
+ /* Event Handlers */
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ async _preCreate(data, options, user) {
+ await super._preCreate(data, options, user);
+
+ // Token size category
+ const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"];
+ this.data.token.update({width: s, height: s});
+
+ // Player character configuration
+ if (this.type === "character") {
+ this.data.token.update({vision: true, actorLink: true, disposition: 1});
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ async _preUpdate(changed, options, user) {
+ await super._preUpdate(changed, options, user);
+
+ // Apply changes in Actor size to Token width/height
+ const newSize = foundry.utils.getProperty(changed, "data.traits.size");
+ if (newSize && newSize !== foundry.utils.getProperty(this.data, "data.traits.size")) {
+ let size = CONFIG.SW5E.tokenSizes[newSize];
+ if (!foundry.utils.hasProperty(changed, "token.width")) {
+ changed.token = changed.token || {};
+ changed.token.height = size;
+ changed.token.width = size;
+ }
+ }
+
+ // Reset death save counters
+ const isDead = this.data.data.attributes.hp.value <= 0;
+ if (isDead && foundry.utils.getProperty(changed, "data.attributes.hp.value") > 0) {
+ foundry.utils.setProperty(changed, "data.attributes.death.success", 0);
+ foundry.utils.setProperty(changed, "data.attributes.death.failure", 0);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Assign a class item as the original class for the Actor based on which class has the most levels
+ * @protected
+ */
+ _assignPrimaryClass() {
+ const classes = this.itemTypes.class.sort((a, b) => b.data.data.levels - a.data.data.levels);
+ const newPC = classes[0]?.id || "";
+ return this.update({"data.details.originalClass": newPC});
+ }
+
+ /* -------------------------------------------- */
+ /* Gameplay Mechanics */
+ /* -------------------------------------------- */
+
+ /** @override */
+ async modifyTokenAttribute(attribute, value, isDelta, isBar) {
+ if (attribute === "attributes.hp") {
+ const hp = getProperty(this.data.data, attribute);
+ const delta = isDelta ? -1 * value : hp.value + hp.temp - value;
+ return this.applyDamage(delta);
+ }
+ return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Apply a certain amount of damage or healing to the health pool for Actor
+ * @param {number} amount An amount of damage (positive) or healing (negative) to sustain
+ * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing
+ * @return {Promise} A Promise which resolves once the damage has been applied
+ */
+ async applyDamage(amount = 0, multiplier = 1) {
+ amount = Math.floor(parseInt(amount) * multiplier);
+ const hp = this.data.data.attributes.hp;
+
+ // Deduct damage from temp HP first
+ const tmp = parseInt(hp.temp) || 0;
+ const dt = amount > 0 ? Math.min(tmp, amount) : 0;
+
+ // Remaining goes to health
+ const tmpMax = parseInt(hp.tempmax) || 0;
+ const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax);
+
+ // Update the Actor
+ const updates = {
+ "data.attributes.hp.temp": tmp - dt,
+ "data.attributes.hp.value": dh
+ };
+
+ // Delegate damage application to a hook
+ // TODO replace this in the future with a better modifyTokenAttribute function in the core
+ const allowed = Hooks.call(
+ "modifyTokenAttribute",
+ {
+ attribute: "attributes.hp",
+ value: amount,
+ isDelta: false,
+ isBar: true
+ },
+ updates
+ );
+ return allowed !== false ? this.update(updates) : this;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Roll a Skill Check
+ * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
+ * @param {string} skillId The skill id (e.g. "ins")
+ * @param {Object} options Options which configure how the skill check is rolled
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollSkill(skillId, options = {}) {
+ const skl = this.data.data.skills[skillId];
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+
+ // Compose roll parts and data
+ const parts = ["@mod"];
+ const data = {mod: skl.mod + skl.prof};
+
+ // Ability test bonus
+ if (bonuses.check) {
+ data["checkBonus"] = bonuses.check;
+ parts.push("@checkBonus");
+ }
+
+ // Skill check bonus
+ if (bonuses.skill) {
+ data["skillBonus"] = bonuses.skill;
+ parts.push("@skillBonus");
+ }
+
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
+
+ // Reliable Talent applies to any skill check we have full or better proficiency in
+ const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent");
+
+ // Roll and return
+ const rollData = foundry.utils.mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.format("SW5E.SkillPromptTitle", {
+ skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]
+ }),
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ reliableTalent: reliableTalent,
+ messageData: {
+ "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
+ "flags.sw5e.roll": {type: "skill", skillId}
+ }
});
- chatString = "SW5E.DeathSaveCriticalSuccess";
- }
+ return d20Roll(rollData);
+ }
- // 3 Successes = survive and reset checks
- else if ( successes === 3 ) {
- await this.update({
- "data.attributes.death.success": 0,
- "data.attributes.death.failure": 0
+ /* -------------------------------------------- */
+
+ /**
+ * Roll a generic ability test or saving throw.
+ * Prompt the user for input on which variety of roll they want to do.
+ * @param {String}abilityId The ability id (e.g. "str")
+ * @param {Object} options Options which configure how ability tests or saving throws are rolled
+ */
+ rollAbility(abilityId, options = {}) {
+ const label = CONFIG.SW5E.abilities[abilityId];
+ new Dialog({
+ title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
+ content: `${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}
`,
+ buttons: {
+ test: {
+ label: game.i18n.localize("SW5E.ActionAbil"),
+ callback: () => this.rollAbilityTest(abilityId, options)
+ },
+ save: {
+ label: game.i18n.localize("SW5E.ActionSave"),
+ callback: () => this.rollAbilitySave(abilityId, options)
+ }
+ }
+ }).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Roll an Ability Test
+ * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
+ * @param {String} abilityId The ability ID (e.g. "str")
+ * @param {Object} options Options which configure how ability tests are rolled
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollAbilityTest(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};
+
+ // Add feat-related proficiency bonuses
+ const feats = this.data.flags.sw5e || {};
+ if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) {
+ parts.push("@proficiency");
+ data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof);
+ } else if (feats.jackOfAllTrades) {
+ parts.push("@proficiency");
+ data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof);
+ }
+
+ // Add global actor bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if (bonuses.check) {
+ parts.push("@checkBonus");
+ data.checkBonus = bonuses.check;
+ }
+
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
+
+ // Roll and return
+ const rollData = foundry.utils.mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
+ halflingLucky: feats.halflingLucky,
+ messageData: {
+ "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
+ "flags.sw5e.roll": {type: "ability", abilityId}
+ }
});
- chatString = "SW5E.DeathSaveSuccess";
- }
-
- // Increment successes
- else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)});
+ return d20Roll(rollData);
}
- // Save failure
- else {
- let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1);
- await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)});
- if ( failures >= 3 ) { // 3 Failures = death
- chatString = "SW5E.DeathSaveFailure";
- }
- }
+ /* -------------------------------------------- */
- // Display success/failure chat message
- if ( chatString ) {
- let chatData = { content: game.i18n.format(chatString, {name: this.name}), speaker };
- ChatMessage.applyRollMode(chatData, roll.options.rollMode);
- await ChatMessage.create(chatData);
- }
+ /**
+ * Roll an Ability Saving Throw
+ * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
+ * @param {String} abilityId The ability ID (e.g. "str")
+ * @param {Object} options Options which configure how ability tests are rolled
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollAbilitySave(abilityId, options = {}) {
+ const label = CONFIG.SW5E.abilities[abilityId];
+ const abl = this.data.data.abilities[abilityId];
- // Return the rolled result
- return roll;
- }
+ // Construct parts
+ const parts = ["@mod"];
+ const data = {mod: abl.mod};
- /* -------------------------------------------- */
-
- /**
- * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier
- * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8".
- * If no denomination is provided, the first available HD will be used
- * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll?
- * @return {Promise} The created Roll instance, or null if no hit die was rolled
- */
- async rollHitDie(denomination, {dialog=true}={}) {
-
- // If no denomination was provided, choose the first available
- let cls = null;
- if ( !denomination ) {
- cls = this.itemTypes.class.find(c => c.data.data.hitDiceUsed < c.data.data.levels);
- if ( !cls ) return null;
- denomination = cls.data.data.hitDice;
- }
-
- // Otherwise locate a class (if any) which has an available hit die of the requested denomination
- else {
- cls = this.items.find(i => {
- const d = i.data.data;
- return (d.hitDice === denomination) && ((d.hitDiceUsed || 0) < (d.levels || 1));
- });
- }
-
- // If no class is available, display an error notification
- if ( !cls ) {
- ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination}));
- return null;
- }
-
- // Prepare roll data
- const parts = [`1${denomination}`, "@abilities.con.mod"];
- const title = game.i18n.localize("SW5E.HitDiceRoll");
- const rollData = foundry.utils.deepClone(this.data.data);
-
- // Call the roll helper utility
- const roll = await damageRoll({
- event: new Event("hitDie"),
- parts: parts,
- data: rollData,
- title: title,
- allowCritical: false,
- fastForward: !dialog,
- dialogOptions: {width: 350},
- messageData: {
- speaker: ChatMessage.getSpeaker({actor: this}),
- "flags.sw5e.roll": {type: "hitDie"}
- }
- });
- if ( !roll ) return null;
-
- // Adjust actor data
- await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
- const hp = this.data.data.attributes.hp;
- const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
- await this.update({"data.attributes.hp.value": hp.value + dhp});
- return roll;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Results from a rest operation.
- *
- * @typedef {object} RestResult
- * @property {number} dhp Hit points recovered during the rest.
- * @property {number} dhd Hit dice recovered or spent during the rest.
- * @property {object} updateData Updates applied to the actor.
- * @property {Array.} updateItems Updates applied to actor's items.
- * @property {boolean} newDay Whether a new day occurred during the rest.
- */
-
- /* -------------------------------------------- */
-
- /**
- * Take a short rest, possibly spending hit dice and recovering resources, item uses, and tech slots & points.
- *
- * @param {object} [options]
- * @param {boolean} [options.dialog=true] Present a dialog window which allows for rolling hit dice as part
- * of the Short Rest and selecting whether a new day has occurred.
- * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message.
- * @param {boolean} [options.autoHD=false] Automatically spend Hit Dice if you are missing 3 or more hit points.
- * @param {boolean} [options.autoHDThreshold=3] A number of missing hit points which would trigger an automatic HD roll.
- * @return {Promise.} A Promise which resolves once the short rest workflow has completed.
- */
- async shortRest({dialog=true, chat=true, autoHD=false, autoHDThreshold=3}={}) {
-
- // Take note of the initial hit points and number of hit dice the Actor has
- const hd0 = this.data.data.attributes.hd;
- const hp0 = this.data.data.attributes.hp.value;
- let newDay = false;
-
- // Display a Dialog for rolling hit dice
- if ( dialog ) {
- try {
- newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0});
- } catch(err) {
- return;
- }
- }
-
- // Automatically spend hit dice
- else if ( autoHD ) {
- await this.autoSpendHitDice({ threshold: autoHDThreshold });
- }
-
- return this._rest(chat, newDay, false, this.data.data.attributes.hd - hd0, this.data.data.attributes.hp.value - hp0, this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Take a long rest, recovering hit points, hit dice, resources, item uses, and tech & force power points & slots.
- *
- * @param {object} [options]
- * @param {boolean} [options.dialog=true] Present a confirmation dialog window whether or not to take a long rest.
- * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message.
- * @param {boolean} [options.newDay=true] 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, newDay=true}={}) {
- // Maybe present a confirmation dialog
- if ( dialog ) {
- try {
- newDay = await LongRestDialog.longRestDialog({actor: this});
- } catch(err) {
- return;
- }
- }
-
- return this._rest(chat, newDay, true, 0, 0, this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value, this.data.data.attributes.force.points.max - this.data.data.attributes.force.points.value);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Perform all of the changes needed for a short or long rest.
- *
- * @param {boolean} chat Summarize the results of the rest workflow as a chat message.
- * @param {boolean} newDay Has a new day occurred during this rest?
- * @param {boolean} longRest Is this a long rest?
- * @param {number} [dhd=0] Number of hit dice spent during so far during the rest.
- * @param {number} [dhp=0] Number of hit points recovered so far during the rest.
- * @param {number} [dtp=0] Number of tech points recovered so far during the rest.
- * @param {number} [dfp=0] Number of force points recovered so far during the rest.
- * @return {Promise.} Consolidated results of the rest workflow.
- * @private
- */
- async _rest(chat, newDay, longRest, dhd=0, dhp=0, dtp=0, dfp=0) {
- // TODO: Turn gritty realism into the SW5e longer rests variant rule https://sw5e.com/rules/variantRules/Longer%20Rests
- let hitPointsRecovered = 0;
- let hitPointUpdates = {};
- let hitDiceRecovered = 0;
- let hitDiceUpdates = [];
-
- // Recover hit points & hit dice on long rest
- if ( longRest ) {
- ({ updates: hitPointUpdates, hitPointsRecovered } = this._getRestHitPointRecovery());
- ({ updates: hitDiceUpdates, hitDiceRecovered } = this._getRestHitDiceRecovery());
- }
-
- // Figure out the rest of the changes
- const result = {
- dhd: dhd + hitDiceRecovered,
- dhp: dhp + hitPointsRecovered,
- dtp: dtp,
- dfp: dfp,
- updateData: {
- ...hitPointUpdates,
- ...this._getRestResourceRecovery({ recoverShortRestResources: !longRest, recoverLongRestResources: longRest }),
- ...this._getRestPowerRecovery({ recoverForcePowers: longRest })
- },
- updateItems: [
- ...hitDiceUpdates,
- ...this._getRestItemUsesRecovery({ recoverLongRestUses: longRest, recoverDailyUses: newDay })
- ],
- newDay: newDay
- }
-
- // Perform updates
- await this.update(result.updateData);
- await this.updateEmbeddedDocuments("Item", result.updateItems);
-
- // Display a Chat Message summarizing the rest effects
- if ( chat ) await this._displayRestResultMessage(result, longRest);
-
- // Return data summarizing the rest effects
- return result;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Display a chat message with the result of a rest.
- *
- * @param {RestResult} result Result of the rest operation.
- * @param {boolean} [longRest=false] Is this a long rest?
- * @return {Promise.} Chat message that was created.
- * @protected
- */
- async _displayRestResultMessage(result, longRest=false) {
- const { dhd, dhp, dtp, dfp, newDay } = result;
- const diceRestored = dhd !== 0;
- const healthRestored = dhp !== 0;
- const length = longRest ? "Long" : "Short";
-
- let restFlavor, message;
-
- // Summarize the rest duration
- switch (game.settings.get("sw5e", "restVariant")) {
- case 'normal': restFlavor = (longRest && newDay) ? "SW5E.LongRestOvernight" : `SW5E.${length}RestNormal`; break;
- case 'gritty': restFlavor = (!longRest && newDay) ? "SW5E.ShortRestOvernight" : `SW5E.${length}RestGritty`; break;
- case 'epic': restFlavor = `SW5E.${length}RestEpic`; break;
- }
-
- // Determine the chat message to display
- if (longRest) {
- message = "SW5E.LongRestResult";
- if (dhp !== 0) message += "HP";
- if (dfp !== 0) message += "FP";
- if (dtp !== 0) message += "TP";
- if (dhd !== 0) message += "HD";
- } else {
- message = "SW5E.ShortRestResultShort";
- if ((dhd !== 0) && (dhp !== 0)){
- if (dtp !== 0){
- message = "SW5E.ShortRestResultWithTech";
- }else{
- message = "SW5E.ShortRestResult";
+ // Include proficiency bonus
+ if (abl.prof > 0) {
+ parts.push("@prof");
+ data.prof = abl.prof;
}
- }else{
- if (dtp !== 0){
- message = "SW5E.ShortRestResultOnlyTech";
+
+ // Include a global actor ability save bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if (bonuses.save) {
+ parts.push("@saveBonus");
+ data.saveBonus = bonuses.save;
}
- }
+
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
+
+ // Roll and return
+ const rollData = foundry.utils.mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}),
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ messageData: {
+ "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
+ "flags.sw5e.roll": {type: "save", abilityId}
+ }
+ });
+ return d20Roll(rollData);
}
- // Create a chat message
- let chatData = {
- user: game.user.id,
- speaker: {actor: this, alias: this.name},
- flavor: game.i18n.localize(restFlavor),
- content: game.i18n.format(message, {
- name: this.name,
- dice: longRest ? dhd : -dhd,
- health: dhp,
- tech: dtp,
- force: dfp
- })
- };
- ChatMessage.applyRollMode(chatData, game.settings.get("core", "rollMode"));
- return ChatMessage.create(chatData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Perform a death saving throw, rolling a d20 plus any global save bonuses
+ * @param {Object} options Additional options which modify the roll
+ * @return {Promise} A Promise which resolves to the Roll instance
+ */
+ async rollDeathSave(options = {}) {
+ // Display a warning if we are not at zero HP or if we already have reached 3
+ const death = this.data.data.attributes.death;
+ if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) {
+ ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary"));
+ return null;
+ }
- /**
- * Automatically spend hit dice to recover hit points up to a certain threshold.
- *
- * @param {object} [options]
- * @param {number} [options.threshold=3] A number of missing hit points which would trigger an automatic HD roll.
- * @return {Promise.} Number of hit dice spent.
- */
- async autoSpendHitDice({ threshold=3 }={}) {
- const max = this.data.data.attributes.hp.max + this.data.data.attributes.hp.tempmax;
+ // Evaluate a global saving throw bonus
+ const parts = [];
+ const data = {};
- let diceRolled = 0;
- while ( (this.data.data.attributes.hp.value + threshold) <= max ) {
- const r = await this.rollHitDie(undefined, {dialog: false});
- if ( r === null ) break;
- diceRolled += 1;
+ // Include a global actor ability save bonus
+ const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {};
+ if (bonuses.save) {
+ parts.push("@saveBonus");
+ data.saveBonus = bonuses.save;
+ }
+
+ // Evaluate the roll
+ const rollData = foundry.utils.mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.localize("SW5E.DeathSavingThrow"),
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ targetValue: 10,
+ messageData: {
+ "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
+ "flags.sw5e.roll": {type: "death"}
+ }
+ });
+ const roll = await d20Roll(rollData);
+ if (!roll) return null;
+
+ // Take action depending on the result
+ const success = roll.total >= 10;
+ const d20 = roll.dice[0].total;
+
+ let chatString;
+
+ // Save success
+ if (success) {
+ let successes = (death.success || 0) + 1;
+
+ // Critical Success = revive with 1hp
+ if (d20 === 20) {
+ await this.update({
+ "data.attributes.death.success": 0,
+ "data.attributes.death.failure": 0,
+ "data.attributes.hp.value": 1
+ });
+ chatString = "SW5E.DeathSaveCriticalSuccess";
+ }
+
+ // 3 Successes = survive and reset checks
+ else if (successes === 3) {
+ await this.update({
+ "data.attributes.death.success": 0,
+ "data.attributes.death.failure": 0
+ });
+ chatString = "SW5E.DeathSaveSuccess";
+ }
+
+ // Increment successes
+ else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)});
+ }
+
+ // Save failure
+ else {
+ let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1);
+ await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)});
+ if (failures >= 3) {
+ // 3 Failures = death
+ chatString = "SW5E.DeathSaveFailure";
+ }
+ }
+
+ // Display success/failure chat message
+ if (chatString) {
+ let chatData = {content: game.i18n.format(chatString, {name: this.name}), speaker};
+ ChatMessage.applyRollMode(chatData, roll.options.rollMode);
+ await ChatMessage.create(chatData);
+ }
+
+ // Return the rolled result
+ return roll;
}
- return diceRolled;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier
+ * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8".
+ * If no denomination is provided, the first available HD will be used
+ * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll?
+ * @return {Promise} The created Roll instance, or null if no hit die was rolled
+ */
+ async rollHitDie(denomination, {dialog = true} = {}) {
+ // If no denomination was provided, choose the first available
+ let cls = null;
+ if (!denomination) {
+ cls = this.itemTypes.class.find((c) => c.data.data.hitDiceUsed < c.data.data.levels);
+ if (!cls) return null;
+ denomination = cls.data.data.hitDice;
+ }
- /**
- * Recovers actor hit points and eliminates any temp HP.
- *
- * @param {object} [options]
- * @param {boolean} [options.recoverTemp=true] Reset temp HP to zero.
- * @param {boolean} [options.recoverTempMax=true] Reset temp max HP to zero.
- * @return {object} Updates to the actor and change in hit points.
- * @protected
- */
- _getRestHitPointRecovery({ recoverTemp=true, recoverTempMax=true }={}) {
- const data = this.data.data;
- let updates = {};
- let max = data.attributes.hp.max;
+ // Otherwise locate a class (if any) which has an available hit die of the requested denomination
+ else {
+ cls = this.items.find((i) => {
+ const d = i.data.data;
+ return d.hitDice === denomination && (d.hitDiceUsed || 0) < (d.levels || 1);
+ });
+ }
- if ( recoverTempMax ) {
- updates["data.attributes.hp.tempmax"] = 0;
- } else {
- max += data.attributes.hp.tempmax;
- }
- updates["data.attributes.hp.value"] = max;
- if ( recoverTemp ) {
- updates["data.attributes.hp.temp"] = 0;
+ // If no class is available, display an error notification
+ if (!cls) {
+ ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination}));
+ return null;
+ }
+
+ // Prepare roll data
+ const parts = [`1${denomination}`, "@abilities.con.mod"];
+ const title = game.i18n.localize("SW5E.HitDiceRoll");
+ const rollData = foundry.utils.deepClone(this.data.data);
+
+ // Call the roll helper utility
+ const roll = await damageRoll({
+ event: new Event("hitDie"),
+ parts: parts,
+ data: rollData,
+ title: title,
+ allowCritical: false,
+ fastForward: !dialog,
+ dialogOptions: {width: 350},
+ messageData: {
+ "speaker": ChatMessage.getSpeaker({actor: this}),
+ "flags.sw5e.roll": {type: "hitDie"}
+ }
+ });
+ if (!roll) return null;
+
+ // Adjust actor data
+ await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
+ const hp = this.data.data.attributes.hp;
+ const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
+ await this.update({"data.attributes.hp.value": hp.value + dhp});
+ return roll;
}
- return { updates, hitPointsRecovered: max - data.attributes.hp.value };
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Results from a rest operation.
+ *
+ * @typedef {object} RestResult
+ * @property {number} dhp Hit points recovered during the rest.
+ * @property {number} dhd Hit dice recovered or spent during the rest.
+ * @property {object} updateData Updates applied to the actor.
+ * @property {Array.} updateItems Updates applied to actor's items.
+ * @property {boolean} newDay Whether a new day occurred during the rest.
+ */
- /**
- * Recovers actor resources.
- * @param {object} [options]
- * @param {boolean} [options.recoverShortRestResources=true] Recover resources that recharge on a short rest.
- * @param {boolean} [options.recoverLongRestResources=true] Recover resources that recharge on a long rest.
- * @return {object} Updates to the actor.
- * @protected
- */
- _getRestResourceRecovery({recoverShortRestResources=true, recoverLongRestResources=true}={}) {
- let updates = {};
- for ( let [k, r] of Object.entries(this.data.data.resources) ) {
- if ( Number.isNumeric(r.max) && ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr)) ) {
- updates[`data.resources.${k}.value`] = Number(r.max);
- }
- }
- return updates;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Take a short rest, possibly spending hit dice and recovering resources, item uses, and tech slots & points.
+ *
+ * @param {object} [options]
+ * @param {boolean} [options.dialog=true] Present a dialog window which allows for rolling hit dice as part
+ * of the Short Rest and selecting whether a new day has occurred.
+ * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message.
+ * @param {boolean} [options.autoHD=false] Automatically spend Hit Dice if you are missing 3 or more hit points.
+ * @param {boolean} [options.autoHDThreshold=3] A number of missing hit points which would trigger an automatic HD roll.
+ * @return {Promise.} A Promise which resolves once the short rest workflow has completed.
+ */
+ async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) {
+ // Take note of the initial hit points and number of hit dice the Actor has
+ const hd0 = this.data.data.attributes.hd;
+ const hp0 = this.data.data.attributes.hp.value;
+ let newDay = false;
- /**
- * Recovers power slots.
- *
- * @param longRest = true It's a long rest
- * @return {object} Updates to the actor.
- * @protected
- */
- _getRestPowerRecovery({ recoverTechPowers=true, recoverForcePowers=true }={}) {
- let updates = {};
+ // Display a Dialog for rolling hit dice
+ if (dialog) {
+ try {
+ newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0});
+ } catch (err) {
+ return;
+ }
+ }
- if (recoverTechPowers) {
- updates["data.attributes.tech.points.value"] = this.data.data.attributes.tech.points.max;
- updates["data.attributes.tech.points.temp"] = 0;
- updates["data.attributes.tech.points.tempmax"] = 0;
+ // Automatically spend hit dice
+ else if (autoHD) {
+ await this.autoSpendHitDice({threshold: autoHDThreshold});
+ }
- for (let [k, v] of Object.entries(this.data.data.powers)) {
- updates[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : (v.tmax ?? 0);
- }
+ return this._rest(
+ chat,
+ newDay,
+ false,
+ this.data.data.attributes.hd - hd0,
+ this.data.data.attributes.hp.value - hp0,
+ this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value
+ );
}
- if (recoverForcePowers) {
- updates["data.attributes.force.points.value"] = this.data.data.attributes.force.points.max;
- updates["data.attributes.force.points.temp"] = 0;
- updates["data.attributes.force.points.tempmax"] = 0;
+ /* -------------------------------------------- */
- for ( let [k, v] of Object.entries(this.data.data.powers) ) {
- updates[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : (v.fmax ?? 0);
- }
+ /**
+ * Take a long rest, recovering hit points, hit dice, resources, item uses, and tech & force power points & slots.
+ *
+ * @param {object} [options]
+ * @param {boolean} [options.dialog=true] Present a confirmation dialog window whether or not to take a long rest.
+ * @param {boolean} [options.chat=true] Summarize the results of the rest workflow as a chat message.
+ * @param {boolean} [options.newDay=true] 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, newDay = true} = {}) {
+ // Maybe present a confirmation dialog
+ if (dialog) {
+ try {
+ newDay = await LongRestDialog.longRestDialog({actor: this});
+ } catch (err) {
+ return;
+ }
+ }
+
+ return this._rest(
+ chat,
+ newDay,
+ true,
+ 0,
+ 0,
+ this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value,
+ this.data.data.attributes.force.points.max - this.data.data.attributes.force.points.value
+ );
}
- return updates;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Perform all of the changes needed for a short or long rest.
+ *
+ * @param {boolean} chat Summarize the results of the rest workflow as a chat message.
+ * @param {boolean} newDay Has a new day occurred during this rest?
+ * @param {boolean} longRest Is this a long rest?
+ * @param {number} [dhd=0] Number of hit dice spent during so far during the rest.
+ * @param {number} [dhp=0] Number of hit points recovered so far during the rest.
+ * @param {number} [dtp=0] Number of tech points recovered so far during the rest.
+ * @param {number} [dfp=0] Number of force points recovered so far during the rest.
+ * @return {Promise.} Consolidated results of the rest workflow.
+ * @private
+ */
+ async _rest(chat, newDay, longRest, dhd = 0, dhp = 0, dtp = 0, dfp = 0) {
+ // TODO: Turn gritty realism into the SW5e longer rests variant rule https://sw5e.com/rules/variantRules/Longer%20Rests
+ let hitPointsRecovered = 0;
+ let hitPointUpdates = {};
+ let hitDiceRecovered = 0;
+ let hitDiceUpdates = [];
- /**
- * Recovers class hit dice during a long rest.
- *
- * @param {object} [options]
- * @param {number} [options.maxHitDice] Maximum number of hit dice to recover.
- * @return {object} Array of item updates and number of hit dice recovered.
- * @protected
- */
- _getRestHitDiceRecovery({ maxHitDice=undefined }={}) {
- // Determine the number of hit dice which may be recovered
- if ( maxHitDice === undefined ) {
- maxHitDice = Math.max(Math.floor(this.data.data.details.level / 2), 1);
+ // Recover hit points & hit dice on long rest
+ if (longRest) {
+ ({updates: hitPointUpdates, hitPointsRecovered} = this._getRestHitPointRecovery());
+ ({updates: hitDiceUpdates, hitDiceRecovered} = this._getRestHitDiceRecovery());
+ }
+
+ // Figure out the rest of the changes
+ const result = {
+ dhd: dhd + hitDiceRecovered,
+ dhp: dhp + hitPointsRecovered,
+ dtp: dtp,
+ dfp: dfp,
+ updateData: {
+ ...hitPointUpdates,
+ ...this._getRestResourceRecovery({
+ recoverShortRestResources: !longRest,
+ recoverLongRestResources: longRest
+ }),
+ ...this._getRestPowerRecovery({recoverForcePowers: longRest})
+ },
+ updateItems: [
+ ...hitDiceUpdates,
+ ...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay})
+ ],
+ newDay: newDay
+ };
+
+ // Perform updates
+ await this.update(result.updateData);
+ await this.updateEmbeddedDocuments("Item", result.updateItems);
+
+ // Display a Chat Message summarizing the rest effects
+ if (chat) await this._displayRestResultMessage(result, longRest);
+
+ // Return data summarizing the rest effects
+ return result;
}
- // Sort classes which can recover HD, assuming players prefer recovering larger HD first.
- const sortedClasses = Object.values(this.classes).sort((a, b) => {
- return (parseInt(b.data.data.hitDice.slice(1)) || 0) - (parseInt(a.data.data.hitDice.slice(1)) || 0);
- });
+ /* -------------------------------------------- */
- let updates = [];
- let hitDiceRecovered = 0;
- for ( let item of sortedClasses ) {
- const d = item.data.data;
- if ( (hitDiceRecovered < maxHitDice) && (d.hitDiceUsed > 0) ) {
- let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered);
- hitDiceRecovered += delta;
- updates.push({_id: item.id, "data.hitDiceUsed": d.hitDiceUsed - delta});
- }
+ /**
+ * Display a chat message with the result of a rest.
+ *
+ * @param {RestResult} result Result of the rest operation.
+ * @param {boolean} [longRest=false] Is this a long rest?
+ * @return {Promise.} Chat message that was created.
+ * @protected
+ */
+ async _displayRestResultMessage(result, longRest = false) {
+ const {dhd, dhp, dtp, dfp, newDay} = result;
+ const diceRestored = dhd !== 0;
+ const healthRestored = dhp !== 0;
+ const length = longRest ? "Long" : "Short";
+
+ let restFlavor, message;
+
+ // Summarize the rest duration
+ switch (game.settings.get("sw5e", "restVariant")) {
+ case "normal":
+ restFlavor = longRest && newDay ? "SW5E.LongRestOvernight" : `SW5E.${length}RestNormal`;
+ break;
+ case "gritty":
+ restFlavor = !longRest && newDay ? "SW5E.ShortRestOvernight" : `SW5E.${length}RestGritty`;
+ break;
+ case "epic":
+ restFlavor = `SW5E.${length}RestEpic`;
+ break;
+ }
+
+ // Determine the chat message to display
+ if (longRest) {
+ message = "SW5E.LongRestResult";
+ if (dhp !== 0) message += "HP";
+ if (dfp !== 0) message += "FP";
+ if (dtp !== 0) message += "TP";
+ if (dhd !== 0) message += "HD";
+ } else {
+ message = "SW5E.ShortRestResultShort";
+ if (dhd !== 0 && dhp !== 0) {
+ if (dtp !== 0) {
+ message = "SW5E.ShortRestResultWithTech";
+ } else {
+ message = "SW5E.ShortRestResult";
+ }
+ } else {
+ if (dtp !== 0) {
+ message = "SW5E.ShortRestResultOnlyTech";
+ }
+ }
+ }
+
+ // Create a chat message
+ let chatData = {
+ user: game.user.id,
+ speaker: {actor: this, alias: this.name},
+ flavor: game.i18n.localize(restFlavor),
+ content: game.i18n.format(message, {
+ name: this.name,
+ dice: longRest ? dhd : -dhd,
+ health: dhp,
+ tech: dtp,
+ force: dfp
+ })
+ };
+ ChatMessage.applyRollMode(chatData, game.settings.get("core", "rollMode"));
+ return ChatMessage.create(chatData);
}
- return { updates, hitDiceRecovered };
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Automatically spend hit dice to recover hit points up to a certain threshold.
+ *
+ * @param {object} [options]
+ * @param {number} [options.threshold=3] A number of missing hit points which would trigger an automatic HD roll.
+ * @return {Promise.} Number of hit dice spent.
+ */
+ async autoSpendHitDice({threshold = 3} = {}) {
+ const max = this.data.data.attributes.hp.max + this.data.data.attributes.hp.tempmax;
- /**
- * Recovers item uses during short or long rests.
- *
- * @param {object} [options]
- * @param {boolean} [options.recoverShortRestUses=true] Recover uses for items that recharge after a short rest.
- * @param {boolean} [options.recoverLongRestUses=true] Recover uses for items that recharge after a long rest.
- * @param {boolean} [options.recoverDailyUses=true] Recover uses for items that recharge on a new day.
- * @return {Array.} Array of item updates.
- * @protected
- */
- _getRestItemUsesRecovery({ recoverShortRestUses=true, recoverLongRestUses=true, recoverDailyUses=true }={}) {
- let recovery = [];
- if ( recoverShortRestUses ) recovery.push("sr");
- if ( recoverLongRestUses ) recovery.push("lr");
- if ( recoverDailyUses ) recovery.push("day");
+ let diceRolled = 0;
+ while (this.data.data.attributes.hp.value + threshold <= max) {
+ const r = await this.rollHitDie(undefined, {dialog: false});
+ if (r === null) break;
+ diceRolled += 1;
+ }
- let updates = [];
- for ( let item of this.items ) {
- const d = item.data.data;
- if ( d.uses && recovery.includes(d.uses.per) ) {
- updates.push({_id: item.id, "data.uses.value": d.uses.max});
- }
- if ( recoverLongRestUses && d.recharge && d.recharge.value ) {
- updates.push({_id: item.id, "data.recharge.charged": true});
- }
+ return diceRolled;
}
- return updates;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Recovers actor hit points and eliminates any temp HP.
+ *
+ * @param {object} [options]
+ * @param {boolean} [options.recoverTemp=true] Reset temp HP to zero.
+ * @param {boolean} [options.recoverTempMax=true] Reset temp max HP to zero.
+ * @return {object} Updates to the actor and change in hit points.
+ * @protected
+ */
+ _getRestHitPointRecovery({recoverTemp = true, recoverTempMax = true} = {}) {
+ const data = this.data.data;
+ let updates = {};
+ let max = data.attributes.hp.max;
- /**
- * Transform this Actor into another one.
- *
- * @param {Actor} target The target Actor.
- * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con)
- * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha)
- * @param {boolean} [keepSaves] Keep saving throw proficiencies
- * @param {boolean} [keepSkills] Keep skill proficiencies
- * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies
- * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies
- * @param {boolean} [keepClass] Keep proficiency bonus
- * @param {boolean} [keepFeats] Keep features
- * @param {boolean} [keepPowers] Keep powers
- * @param {boolean} [keepItems] Keep items
- * @param {boolean} [keepBio] Keep biography
- * @param {boolean} [keepVision] Keep vision
- * @param {boolean} [transformTokens] Transform linked tokens too
- */
- async transformInto(target, { keepPhysical=false, keepMental=false, keepSaves=false, keepSkills=false,
- mergeSaves=false, mergeSkills=false, keepClass=false, keepFeats=false, keepPowers=false,
- keepItems=false, keepBio=false, keepVision=false, transformTokens=true}={}) {
+ if (recoverTempMax) {
+ updates["data.attributes.hp.tempmax"] = 0;
+ } else {
+ max += data.attributes.hp.tempmax;
+ }
+ updates["data.attributes.hp.value"] = max;
+ if (recoverTemp) {
+ updates["data.attributes.hp.temp"] = 0;
+ }
- // Ensure the player is allowed to polymorph
- const allowed = game.settings.get("sw5e", "allowPolymorphing");
- if ( !allowed && !game.user.isGM ) {
- return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn"));
+ return {updates, hitPointsRecovered: max - data.attributes.hp.value};
}
- // Get the original Actor data and the new source data
- const o = this.toJSON();
- o.flags.sw5e = o.flags.sw5e || {};
- o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves};
- const source = target.toJSON();
+ /* -------------------------------------------- */
- // Prepare new data to merge from the source
- const d = {
- type: o.type, // Remain the same actor type
- name: `${o.name} (${source.name})`, // Append the new shape to your old name
- data: source.data, // Get the data model of your new form
- items: source.items, // Get the items of your new form
- effects: o.effects.concat(source.effects), // Combine active effects from both forms
- img: source.img, // New appearance
- permission: o.permission, // Use the original actor permissions
- folder: o.folder, // Be displayed in the same sidebar folder
- flags: o.flags // Use the original actor flags
- };
-
- // Specifically delete some data attributes
- delete d.data.resources; // Don't change your resource pools
- delete d.data.currency; // Don't lose currency
- delete d.data.bonuses; // Don't lose global bonuses
-
- // Specific additional adjustments
- d.data.details.alignment = o.data.details.alignment; // Don't change alignment
- d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level
- d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration
- d.data.powers = o.data.powers; // Keep power slots
-
- // Token appearance updates
- d.token = {name: d.name};
- for ( let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"] ) {
- d.token[k] = source.token[k];
- }
- if ( !keepVision ) {
- for ( let k of ['dimSight', 'brightSight', 'dimLight', 'brightLight', 'vision', 'sightAngle'] ) {
- d.token[k] = source.token[k];
- }
- }
- if ( source.token.randomImg ) {
- const images = await target.getTokenImages();
- d.token.img = images[Math.floor(Math.random() * images.length)];
+ /**
+ * Recovers actor resources.
+ * @param {object} [options]
+ * @param {boolean} [options.recoverShortRestResources=true] Recover resources that recharge on a short rest.
+ * @param {boolean} [options.recoverLongRestResources=true] Recover resources that recharge on a long rest.
+ * @return {object} Updates to the actor.
+ * @protected
+ */
+ _getRestResourceRecovery({recoverShortRestResources = true, recoverLongRestResources = true} = {}) {
+ let updates = {};
+ for (let [k, r] of Object.entries(this.data.data.resources)) {
+ if (
+ Number.isNumeric(r.max) &&
+ ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr))
+ ) {
+ updates[`data.resources.${k}.value`] = Number(r.max);
+ }
+ }
+ return updates;
}
- // Transfer ability scores
- const abilities = d.data.abilities;
- for ( let k of Object.keys(abilities) ) {
- const oa = o.data.abilities[k];
- const prof = abilities[k].proficient;
- if ( keepPhysical && ["str", "dex", "con"].includes(k) ) abilities[k] = oa;
- else if ( keepMental && ["int", "wis", "cha"].includes(k) ) abilities[k] = oa;
- if ( keepSaves ) abilities[k].proficient = oa.proficient;
- else if ( mergeSaves ) abilities[k].proficient = Math.max(prof, oa.proficient);
+ /* -------------------------------------------- */
+
+ /**
+ * Recovers power slots.
+ *
+ * @param longRest = true It's a long rest
+ * @return {object} Updates to the actor.
+ * @protected
+ */
+ _getRestPowerRecovery({recoverTechPowers = true, recoverForcePowers = true} = {}) {
+ let updates = {};
+
+ if (recoverTechPowers) {
+ updates["data.attributes.tech.points.value"] = this.data.data.attributes.tech.points.max;
+ updates["data.attributes.tech.points.temp"] = 0;
+ updates["data.attributes.tech.points.tempmax"] = 0;
+
+ for (let [k, v] of Object.entries(this.data.data.powers)) {
+ updates[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : v.tmax ?? 0;
+ }
+ }
+
+ if (recoverForcePowers) {
+ updates["data.attributes.force.points.value"] = this.data.data.attributes.force.points.max;
+ updates["data.attributes.force.points.temp"] = 0;
+ updates["data.attributes.force.points.tempmax"] = 0;
+
+ for (let [k, v] of Object.entries(this.data.data.powers)) {
+ updates[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : v.fmax ?? 0;
+ }
+ }
+
+ return updates;
}
- // Transfer skills
- if ( keepSkills ) d.data.skills = o.data.skills;
- else if ( mergeSkills ) {
- for ( let [k, s] of Object.entries(d.data.skills) ) {
- s.value = Math.max(s.value, o.data.skills[k].value);
- }
+ /* -------------------------------------------- */
+
+ /**
+ * Recovers class hit dice during a long rest.
+ *
+ * @param {object} [options]
+ * @param {number} [options.maxHitDice] Maximum number of hit dice to recover.
+ * @return {object} Array of item updates and number of hit dice recovered.
+ * @protected
+ */
+ _getRestHitDiceRecovery({maxHitDice = undefined} = {}) {
+ // Determine the number of hit dice which may be recovered
+ if (maxHitDice === undefined) {
+ maxHitDice = Math.max(Math.floor(this.data.data.details.level / 2), 1);
+ }
+
+ // Sort classes which can recover HD, assuming players prefer recovering larger HD first.
+ const sortedClasses = Object.values(this.classes).sort((a, b) => {
+ return (parseInt(b.data.data.hitDice.slice(1)) || 0) - (parseInt(a.data.data.hitDice.slice(1)) || 0);
+ });
+
+ let updates = [];
+ let hitDiceRecovered = 0;
+ for (let item of sortedClasses) {
+ const d = item.data.data;
+ if (hitDiceRecovered < maxHitDice && d.hitDiceUsed > 0) {
+ let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered);
+ hitDiceRecovered += delta;
+ updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta});
+ }
+ }
+
+ return {updates, hitDiceRecovered};
}
- // Keep specific items from the original data
- d.items = d.items.concat(o.items.filter(i => {
- if ( i.type === "class" ) return keepClass;
- else if ( i.type === "feat" ) return keepFeats;
- else if ( i.type === "power" ) return keepPowers;
- else return keepItems;
- }));
+ /* -------------------------------------------- */
- // Transfer classes for NPCs
- if (!keepClass && d.data.details.cr) {
- d.items.push({
- type: 'class',
- name: game.i18n.localize('SW5E.PolymorphTmpClass'),
- data: { levels: d.data.details.cr }
- });
+ /**
+ * Recovers item uses during short or long rests.
+ *
+ * @param {object} [options]
+ * @param {boolean} [options.recoverShortRestUses=true] Recover uses for items that recharge after a short rest.
+ * @param {boolean} [options.recoverLongRestUses=true] Recover uses for items that recharge after a long rest.
+ * @param {boolean} [options.recoverDailyUses=true] Recover uses for items that recharge on a new day.
+ * @return {Array.} Array of item updates.
+ * @protected
+ */
+ _getRestItemUsesRecovery({recoverShortRestUses = true, recoverLongRestUses = true, recoverDailyUses = true} = {}) {
+ let recovery = [];
+ if (recoverShortRestUses) recovery.push("sr");
+ if (recoverLongRestUses) recovery.push("lr");
+ if (recoverDailyUses) recovery.push("day");
+
+ let updates = [];
+ for (let item of this.items) {
+ const d = item.data.data;
+ if (d.uses && recovery.includes(d.uses.per)) {
+ updates.push({"_id": item.id, "data.uses.value": d.uses.max});
+ }
+ if (recoverLongRestUses && d.recharge && d.recharge.value) {
+ updates.push({"_id": item.id, "data.recharge.charged": true});
+ }
+ }
+
+ return updates;
}
- // Keep biography
- if (keepBio) d.data.details.biography = o.data.details.biography;
+ /* -------------------------------------------- */
- // Keep senses
- if (keepVision) d.data.traits.senses = o.data.traits.senses;
-
- // Set new data flags
- if ( !this.isPolymorphed || !d.flags.sw5e.originalActor ) d.flags.sw5e.originalActor = this.id;
- d.flags.sw5e.isPolymorphed = true;
-
- // Update unlinked Tokens in place since they can simply be re-dropped from the base actor
- if (this.isToken) {
- const tokenData = d.token;
- tokenData.actorData = d;
- delete tokenData.actorData.token;
- return this.token.update(tokenData);
- }
-
- // Update regular Actors by creating a new Actor with the Polymorphed data
- await this.sheet.close();
- Hooks.callAll('sw5e.transformActor', this, target, d, {
- keepPhysical, keepMental, keepSaves, keepSkills, mergeSaves, mergeSkills,
- keepClass, keepFeats, keepPowers, keepItems, keepBio, keepVision, transformTokens
- });
- const newActor = await this.constructor.create(d, {renderSheet: true});
-
- // Update placed Token instances
- if ( !transformTokens ) return;
- const tokens = this.getActiveTokens(true);
- const updates = tokens.map(t => {
- const newTokenData = foundry.utils.deepClone(d.token);
- if ( !t.data.actorLink ) newTokenData.actorData = newActor.data;
- newTokenData._id = t.data._id;
- newTokenData.actorId = newActor.id;
- return newTokenData;
- });
- return canvas.scene?.updateEmbeddedDocuments("Token", updates);
- }
-
- /* -------------------------------------------- */
-
- /**
- * If this actor was transformed with transformTokens enabled, then its
- * active tokens need to be returned to their original state. If not, then
- * we can safely just delete this actor.
- */
- async revertOriginalForm() {
- if ( !this.isPolymorphed ) return;
- if ( !this.isOwner ) {
- return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn"));
- }
-
- // If we are reverting an unlinked token, simply replace it with the base actor prototype
- if ( this.isToken ) {
- const baseActor = game.actors.get(this.token.data.actorId);
- const prototypeTokenData = await baseActor.getTokenData();
- const tokenUpdate = {actorData: {}};
- for ( let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"] ) {
- tokenUpdate[k] = prototypeTokenData[k];
- }
- return this.token.update(tokenUpdate, {recursive: false});
- }
-
- // Obtain a reference to the original actor
- const original = game.actors.get(this.getFlag('sw5e', 'originalActor'));
- if ( !original ) return;
-
- // Get the Tokens which represent this actor
- if ( canvas.ready ) {
- const tokens = this.getActiveTokens(true);
- const tokenData = await original.getTokenData();
- const tokenUpdates = tokens.map(t => {
- const update = duplicate(tokenData);
- update._id = t.id;
- delete update.x;
- delete update.y;
- return update;
- });
- canvas.scene.updateEmbeddedDocuments("Token", tokenUpdates);
- }
-
- // Delete the polymorphed version of the actor, if possible
- const isRendered = this.sheet.rendered;
- if ( game.user.isGM ) await this.delete();
- else if ( isRendered ) this.sheet.close();
- if ( isRendered ) original.sheet.render(isRendered);
- return original;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Add additional system-specific sidebar directory context menu options for SW5e Actor entities
- * @param {jQuery} html The sidebar HTML
- * @param {Array} entryOptions The default array of context menu options
- */
- static addDirectoryContextOptions(html, entryOptions) {
- entryOptions.push({
- name: 'SW5E.PolymorphRestoreTransformation',
- icon: ' ',
- callback: li => {
- const actor = game.actors.get(li.data('entityId'));
- return actor.revertOriginalForm();
- },
- condition: li => {
+ /**
+ * Transform this Actor into another one.
+ *
+ * @param {Actor} target The target Actor.
+ * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con)
+ * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha)
+ * @param {boolean} [keepSaves] Keep saving throw proficiencies
+ * @param {boolean} [keepSkills] Keep skill proficiencies
+ * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies
+ * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies
+ * @param {boolean} [keepClass] Keep proficiency bonus
+ * @param {boolean} [keepFeats] Keep features
+ * @param {boolean} [keepPowers] Keep powers
+ * @param {boolean} [keepItems] Keep items
+ * @param {boolean} [keepBio] Keep biography
+ * @param {boolean} [keepVision] Keep vision
+ * @param {boolean} [transformTokens] Transform linked tokens too
+ */
+ async transformInto(
+ target,
+ {
+ keepPhysical = false,
+ keepMental = false,
+ keepSaves = false,
+ keepSkills = false,
+ mergeSaves = false,
+ mergeSkills = false,
+ keepClass = false,
+ keepFeats = false,
+ keepPowers = false,
+ keepItems = false,
+ keepBio = false,
+ keepVision = false,
+ transformTokens = true
+ } = {}
+ ) {
+ // Ensure the player is allowed to polymorph
const allowed = game.settings.get("sw5e", "allowPolymorphing");
- if ( !allowed && !game.user.isGM ) return false;
- const actor = game.actors.get(li.data('entityId'));
- return actor && actor.isPolymorphed;
- }
- });
- }
+ if (!allowed && !game.user.isGM) {
+ return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn"));
+ }
- /* -------------------------------------------- */
+ // Get the original Actor data and the new source data
+ const o = this.toJSON();
+ o.flags.sw5e = o.flags.sw5e || {};
+ o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves};
+ const source = target.toJSON();
- /**
- * Format a type object into a string.
- * @param {object} typeData The type data to convert to a string.
- * @returns {string}
- */
- static formatCreatureType(typeData) {
- if ( typeof typeData === "string" ) return typeData; // backwards compatibility
- let localizedType;
- if ( typeData.value === "custom" ) {
- localizedType = typeData.custom;
- } else {
- let code = CONFIG.SW5E.creatureTypes[typeData.value];
- localizedType = game.i18n.localize(!!typeData.swarm ? `${code}Pl` : code);
+ // Prepare new data to merge from the source
+ const d = {
+ type: o.type, // Remain the same actor type
+ name: `${o.name} (${source.name})`, // Append the new shape to your old name
+ data: source.data, // Get the data model of your new form
+ items: source.items, // Get the items of your new form
+ effects: o.effects.concat(source.effects), // Combine active effects from both forms
+ img: source.img, // New appearance
+ permission: o.permission, // Use the original actor permissions
+ folder: o.folder, // Be displayed in the same sidebar folder
+ flags: o.flags // Use the original actor flags
+ };
+
+ // Specifically delete some data attributes
+ delete d.data.resources; // Don't change your resource pools
+ delete d.data.currency; // Don't lose currency
+ delete d.data.bonuses; // Don't lose global bonuses
+
+ // Specific additional adjustments
+ d.data.details.alignment = o.data.details.alignment; // Don't change alignment
+ d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level
+ d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration
+ d.data.powers = o.data.powers; // Keep power slots
+
+ // Token appearance updates
+ d.token = {name: d.name};
+ for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) {
+ d.token[k] = source.token[k];
+ }
+ if (!keepVision) {
+ for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) {
+ d.token[k] = source.token[k];
+ }
+ }
+ if (source.token.randomImg) {
+ const images = await target.getTokenImages();
+ d.token.img = images[Math.floor(Math.random() * images.length)];
+ }
+
+ // Transfer ability scores
+ const abilities = d.data.abilities;
+ for (let k of Object.keys(abilities)) {
+ const oa = o.data.abilities[k];
+ const prof = abilities[k].proficient;
+ if (keepPhysical && ["str", "dex", "con"].includes(k)) abilities[k] = oa;
+ else if (keepMental && ["int", "wis", "cha"].includes(k)) abilities[k] = oa;
+ if (keepSaves) abilities[k].proficient = oa.proficient;
+ else if (mergeSaves) abilities[k].proficient = Math.max(prof, oa.proficient);
+ }
+
+ // Transfer skills
+ if (keepSkills) d.data.skills = o.data.skills;
+ else if (mergeSkills) {
+ for (let [k, s] of Object.entries(d.data.skills)) {
+ s.value = Math.max(s.value, o.data.skills[k].value);
+ }
+ }
+
+ // Keep specific items from the original data
+ d.items = d.items.concat(
+ o.items.filter((i) => {
+ if (i.type === "class") return keepClass;
+ else if (i.type === "feat") return keepFeats;
+ else if (i.type === "power") return keepPowers;
+ else return keepItems;
+ })
+ );
+
+ // Transfer classes for NPCs
+ if (!keepClass && d.data.details.cr) {
+ d.items.push({
+ type: "class",
+ name: game.i18n.localize("SW5E.PolymorphTmpClass"),
+ data: {levels: d.data.details.cr}
+ });
+ }
+
+ // Keep biography
+ if (keepBio) d.data.details.biography = o.data.details.biography;
+
+ // Keep senses
+ if (keepVision) d.data.traits.senses = o.data.traits.senses;
+
+ // Set new data flags
+ if (!this.isPolymorphed || !d.flags.sw5e.originalActor) d.flags.sw5e.originalActor = this.id;
+ d.flags.sw5e.isPolymorphed = true;
+
+ // Update unlinked Tokens in place since they can simply be re-dropped from the base actor
+ if (this.isToken) {
+ const tokenData = d.token;
+ tokenData.actorData = d;
+ delete tokenData.actorData.token;
+ return this.token.update(tokenData);
+ }
+
+ // Update regular Actors by creating a new Actor with the Polymorphed data
+ await this.sheet.close();
+ Hooks.callAll("sw5e.transformActor", this, target, d, {
+ keepPhysical,
+ keepMental,
+ keepSaves,
+ keepSkills,
+ mergeSaves,
+ mergeSkills,
+ keepClass,
+ keepFeats,
+ keepPowers,
+ keepItems,
+ keepBio,
+ keepVision,
+ transformTokens
+ });
+ const newActor = await this.constructor.create(d, {renderSheet: true});
+
+ // Update placed Token instances
+ if (!transformTokens) return;
+ const tokens = this.getActiveTokens(true);
+ const updates = tokens.map((t) => {
+ const newTokenData = foundry.utils.deepClone(d.token);
+ if (!t.data.actorLink) newTokenData.actorData = newActor.data;
+ newTokenData._id = t.data._id;
+ newTokenData.actorId = newActor.id;
+ return newTokenData;
+ });
+ return canvas.scene?.updateEmbeddedDocuments("Token", updates);
}
- let type = localizedType;
- if ( !!typeData.swarm ) {
- type = game.i18n.format('SW5E.CreatureSwarmPhrase', {
- size: game.i18n.localize(CONFIG.SW5E.actorSizes[typeData.swarm]),
- type: localizedType
- });
+
+ /* -------------------------------------------- */
+
+ /**
+ * If this actor was transformed with transformTokens enabled, then its
+ * active tokens need to be returned to their original state. If not, then
+ * we can safely just delete this actor.
+ */
+ async revertOriginalForm() {
+ if (!this.isPolymorphed) return;
+ if (!this.isOwner) {
+ return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn"));
+ }
+
+ // If we are reverting an unlinked token, simply replace it with the base actor prototype
+ if (this.isToken) {
+ const baseActor = game.actors.get(this.token.data.actorId);
+ const prototypeTokenData = await baseActor.getTokenData();
+ const tokenUpdate = {actorData: {}};
+ for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) {
+ tokenUpdate[k] = prototypeTokenData[k];
+ }
+ return this.token.update(tokenUpdate, {recursive: false});
+ }
+
+ // Obtain a reference to the original actor
+ const original = game.actors.get(this.getFlag("sw5e", "originalActor"));
+ if (!original) return;
+
+ // Get the Tokens which represent this actor
+ if (canvas.ready) {
+ const tokens = this.getActiveTokens(true);
+ const tokenData = await original.getTokenData();
+ const tokenUpdates = tokens.map((t) => {
+ const update = duplicate(tokenData);
+ update._id = t.id;
+ delete update.x;
+ delete update.y;
+ return update;
+ });
+ canvas.scene.updateEmbeddedDocuments("Token", tokenUpdates);
+ }
+
+ // Delete the polymorphed version of the actor, if possible
+ const isRendered = this.sheet.rendered;
+ if (game.user.isGM) await this.delete();
+ else if (isRendered) this.sheet.close();
+ if (isRendered) original.sheet.render(isRendered);
+ return original;
}
- if (typeData.subtype) type = `${type} (${typeData.subtype})`;
- return type;
- }
- /* -------------------------------------------- */
- /* 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;
- }
+ /**
+ * Add additional system-specific sidebar directory context menu options for SW5e Actor entities
+ * @param {jQuery} html The sidebar HTML
+ * @param {Array} entryOptions The default array of context menu options
+ */
+ static addDirectoryContextOptions(html, entryOptions) {
+ entryOptions.push({
+ name: "SW5E.PolymorphRestoreTransformation",
+ icon: ' ',
+ callback: (li) => {
+ const actor = game.actors.get(li.data("entityId"));
+ return actor.revertOriginalForm();
+ },
+ condition: (li) => {
+ const allowed = game.settings.get("sw5e", "allowPolymorphing");
+ if (!allowed && !game.user.isGM) return false;
+ const actor = game.actors.get(li.data("entityId"));
+ return actor && actor.isPolymorphed;
+ }
+ });
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Cast a Power, consuming a power slot of a certain level
- * @param {Item5e} item The power being cast by the actor
- * @param {Event} event The originating user interaction which triggered the cast
- * @deprecated since sw5e 1.2.0
- */
- async usePower(item, {configureDialog=true}={}) {
- console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
- if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
- return item.roll();
- }
-}
\ No newline at end of file
+ /**
+ * Format a type object into a string.
+ * @param {object} typeData The type data to convert to a string.
+ * @returns {string}
+ */
+ static formatCreatureType(typeData) {
+ if (typeof typeData === "string") return typeData; // backwards compatibility
+ let localizedType;
+ if (typeData.value === "custom") {
+ localizedType = typeData.custom;
+ } else {
+ let code = CONFIG.SW5E.creatureTypes[typeData.value];
+ localizedType = game.i18n.localize(!!typeData.swarm ? `${code}Pl` : code);
+ }
+ let type = localizedType;
+ if (!!typeData.swarm) {
+ type = game.i18n.format("SW5E.CreatureSwarmPhrase", {
+ size: game.i18n.localize(CONFIG.SW5E.actorSizes[typeData.swarm]),
+ type: localizedType
+ });
+ }
+ if (typeData.subtype) type = `${type} (${typeData.subtype})`;
+ return type;
+ }
+
+ /* -------------------------------------------- */
+ /* 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;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Cast a Power, consuming a power slot of a certain level
+ * @param {Item5e} item The power being cast by the actor
+ * @param {Event} event The originating user interaction which triggered the cast
+ * @deprecated since sw5e 1.2.0
+ */
+ async usePower(item, {configureDialog = true} = {}) {
+ console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
+ if (item.data.type !== "power") throw new Error("Wrong Item type");
+ return item.roll();
+ }
+}
diff --git a/module/actor/old_entity.js b/module/actor/old_entity.js
index 23080d27..50b9b895 100644
--- a/module/actor/old_entity.js
+++ b/module/actor/old_entity.js
@@ -1,2040 +1,2126 @@
-import { d20Roll, damageRoll } from "../dice.js";
+import {d20Roll, damageRoll} from "../dice.js";
import ShortRestDialog from "../apps/short-rest.js";
import LongRestDialog from "../apps/long-rest.js";
-import {SW5E} from '../config.js';
+import {SW5E} from "../config.js";
/**
* Extend the base Actor class to implement additional system-specific logic for SW5e.
*/
export default class Actor5e extends Actor {
-
- /**
- * Is this Actor currently polymorphed into some other creature?
- * @return {boolean}
- */
- get isPolymorphed() {
- return this.getFlag("sw5e", "isPolymorphed") || false;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- prepareBaseData() {
- switch ( this.data.type ) {
- case "character":
- return this._prepareCharacterData(this.data);
- case "npc":
- return this._prepareNPCData(this.data);
- case "starship":
- return this._prepareStarshipData(this.data);
- case "vehicle":
- return this._prepareVehicleData(this.data);
- }
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- prepareDerivedData() {
- const actorData = this.data;
- const data = actorData.data;
- const flags = actorData.flags.sw5e || {};
- const bonuses = getProperty(data, "bonuses.abilities") || {};
-
- // Retrieve data for polymorphed actors
- let originalSaves = null;
- let originalSkills = null;
- if (this.isPolymorphed) {
- const transformOptions = this.getFlag('sw5e', 'transformOptions');
- const original = game.actors?.get(this.getFlag('sw5e', 'originalActor'));
- if (original) {
- if (transformOptions.mergeSaves) {
- originalSaves = original.data.data.abilities;
- }
- if (transformOptions.mergeSkills) {
- originalSkills = original.data.data.skills;
- }
- }
+ /**
+ * Is this Actor currently polymorphed into some other creature?
+ * @return {boolean}
+ */
+ get isPolymorphed() {
+ return this.getFlag("sw5e", "isPolymorphed") || false;
}
- // 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)) {
- abl.mod = Math.floor((abl.value - 10) / 2);
- abl.prof = (abl.proficient || 0) * data.attributes.prof;
- abl.saveBonus = saveBonus;
- abl.checkBonus = checkBonus;
- abl.save = abl.mod + abl.prof + abl.saveBonus;
- abl.dc = 8 + abl.mod + data.attributes.prof + dcBonus;
-
- // If we merged saves when transforming, take the highest bonus here.
- if (originalSaves && abl.proficient) {
- abl.save = Math.max(abl.save, originalSaves[id].save);
- }
- }
-
- // Inventory encumbrance
- data.attributes.encumbrance = this._computeEncumbrance(actorData);
-
- if (actorData.type === "starship") {
-
- // Calculate AC
- data.attributes.ac.value += Math.min(data.abilities.dex.mod, data.attributes.equip.armor.maxDex);
-
- // Set Power Die Storage
- data.attributes.power.central.max += data.attributes.equip.powerCoupling.centralCap;
- data.attributes.power.comms.max += data.attributes.equip.powerCoupling.systemCap;
- data.attributes.power.engines.max += data.attributes.equip.powerCoupling.systemCap;
- data.attributes.power.shields.max += data.attributes.equip.powerCoupling.systemCap;
- data.attributes.power.sensors.max += data.attributes.equip.powerCoupling.systemCap;
- data.attributes.power.weapons.max += data.attributes.equip.powerCoupling.systemCap;
-
- // Find Size info of Starship
- const size = actorData.items.filter(i => i.type === "starship");
- if (size.length === 0) return;
- const sizeData = size[0].data;
-
- // Prepare Hull Points
- data.attributes.hp.max = sizeData.hullDiceRolled.reduce((a, b) => a + b, 0) + data.abilities.con.mod * data.attributes.hull.dicemax;
- if (data.attributes.hp.value === null) data.attributes.hp.value = data.attributes.hp.max;
-
- // Prepare Shield Points
- data.attributes.hp.tempmax = (sizeData.shldDiceRolled.reduce((a, b) => a + b, 0) + data.abilities.str.mod * data.attributes.shld.dicemax) * data.attributes.equip.shields.capMult;
- if (data.attributes.hp.temp === null) data.attributes.hp.temp = data.attributes.hp.tempmax;
-
- // Prepare Speeds
- data.attributes.movement.space = sizeData.baseSpaceSpeed + (50 * (data.abilities.str.mod - data.abilities.con.mod));
- data.attributes.movement.turn = Math.min(data.attributes.movement.space, Math.max(50,(sizeData.baseTurnSpeed - (50 * (data.abilities.dex.mod - data.abilities.con.mod)))));
-
- // Prepare Max Suites
- data.attributes.mods.suites.max = sizeData.modMaxSuitesBase + (sizeData.modMaxSuitesMult * data.abilities.con.mod);
-
- // Prepare Hardpoints
- data.attributes.mods.hardpoints.max = sizeData.hardpointMult * Math.max(1,data.abilities.str.mod);
-
- //Prepare Fuel
- data.attributes.fuel = this._computeFuel(actorData);
- }
-
- // Prepare skills
- this._prepareSkills(actorData, bonuses, checkBonus, originalSkills);
-
- // Determine Initiative Modifier
- const init = data.attributes.init;
- const athlete = flags.remarkableAthlete;
- const joat = flags.jackOfAllTrades;
- init.mod = data.abilities.dex.mod;
- 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.value = init.value ?? 0;
- init.bonus = init.value + (flags.initiativeAlert ? 5 : 0);
- init.total = init.mod + init.prof + init.bonus;
-
- // Prepare power-casting data
- data.attributes.powerForceLightDC = 8 + data.abilities.wis.mod + data.attributes.prof ?? 10;
- data.attributes.powerForceDarkDC = 8 + data.abilities.cha.mod + data.attributes.prof ?? 10;
- data.attributes.powerForceUnivDC = Math.max(data.attributes.powerForceLightDC,data.attributes.powerForceDarkDC) ?? 10;
- data.attributes.powerTechDC = 8 + data.abilities.int.mod + data.attributes.prof ?? 10;
- this._computeDerivedPowercasting(this.data);
-
- // Compute owned item attributes which depend on prepared Actor data
- this.items.forEach(item => {
- item.getSaveDC();
- item.getAttackToHit();
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return the amount of experience required to gain a certain character level.
- * @param level {Number} The desired level
- * @return {Number} The XP required
- */
- getLevelExp(level) {
- const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS;
- return levels[Math.min(level, levels.length - 1)];
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return the amount of experience granted by killing a creature of a certain CR.
- * @param cr {Number} The creature's challenge rating
- * @return {Number} The amount of experience granted per kill
- */
- getCRExp(cr) {
- if (cr < 1.0) return Math.max(200 * cr, 10);
- return CONFIG.SW5E.CR_EXP_LEVELS[cr];
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getRollData() {
- const data = super.getRollData();
- data.classes = this.data.items.reduce((obj, i) => {
- if ( i.type === "class" ) {
- obj[i.name.slugify({strict: true})] = i.data;
- }
- return obj;
- }, {});
- data.prof = this.data.data.attributes.prof || 0;
- return data;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return the features which a character is awarded for each class level
- * @param {string} className The class name being added
- * @param {string} archetypeName The archetype 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} Array of Item5e entities
- */
- static async getClassFeatures({className="", archetypeName="", level=1, priorLevel=0}={}) {
- className = className.toLowerCase();
- archetypeName = archetypeName.slugify();
-
- // Get the configuration of features which may be added
- const clsConfig = CONFIG.SW5E.classFeatures[className];
- if (!clsConfig) return [];
-
- // 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 archetype features
- const archConfig = clsConfig.archetypes[archetypeName] || {};
- for ( let [l, f] of Object.entries(archConfig.features || {}) ) {
- l = parseInt(l);
- if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
- }
-
- // Load item data for all identified features
- const features = [];
- for ( let id of ids ) {
- features.push(await 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 updateData = expandObject(u);
- const config = {
- className: updateData.name || item.data.name,
- archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype,
- level: getProperty(updateData, "data.levels"),
- priorLevel: item ? item.data.data.levels : 0
- }
-
- // Get and create features for an increased class level
- let changed = false;
- if ( config.level && (config.level > config.priorLevel)) changed = true;
- if ( config.archetypeName !== item.data.data.archetype ) changed = true;
-
- // Get features to create
- if ( changed ) {
- 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);
- }
- }
- }
- return toCreate
- }
-
- /* -------------------------------------------- */
- /* Data Preparation Helpers */
- /* -------------------------------------------- */
-
- /**
- * Prepare Character type specific data
- */
- _prepareCharacterData(actorData) {
- const data = actorData.data;
-
- // Determine character level and available hit dice based on owned Class items
- const [level, hd] = actorData.items.reduce((arr, item) => {
- if ( item.type === "class" ) {
- const classLevels = parseInt(item.data.levels) || 1;
- arr[0] += classLevels;
- arr[1] += classLevels - (parseInt(item.data.hitDiceUsed) || 0);
- }
- return arr;
- }, [0, 0]);
- data.details.level = level;
- data.attributes.hd = hd;
-
- // Character proficiency bonus
- data.attributes.prof = Math.floor((level + 7) / 4);
-
- // Experience required for next level
- const xp = data.details.xp;
- xp.max = this.getLevelExp(level || 1);
- const prior = this.getLevelExp(level - 1 || 0);
- const required = xp.max - prior;
- const pct = Math.round((xp.value - prior) * 100 / required);
- xp.pct = Math.clamped(pct, 0, 100);
-
- // Add base Powercasting attributes
- this._computeBasePowercasting(actorData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare NPC type specific data
- */
- _prepareNPCData(actorData) {
- const data = actorData.data;
-
- // Kill Experience
- data.details.xp.value = this.getCRExp(data.details.cr);
-
- // Proficiency
- data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4);
-
- this._computeBasePowercasting(actorData);
-
- // Powercaster Level
- if ( data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel) ) {
- data.details.powerLevel = Math.max(data.details.cr, 1);
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare vehicle type-specific data
- * @param actorData
- * @private
- */
- _prepareVehicleData(actorData) {}
-
- /* -------------------------------------------- */
-
- /* -------------------------------------------- */
-
- /**
- * Prepare starship type-specific data
- * @param actorData
- * @private
- */
- _prepareStarshipData(actorData) {
-
- const data = actorData.data;
- data.attributes.prof = 0;
- // Determine Starship size-based properties based on owned Starship item
- const size = actorData.items.filter(i => i.type === "starship");
- if (size.length !== 0) {
- const sizeData = size[0].data;
- const tiers = parseInt(sizeData.tier) || 0;
- data.traits.size = sizeData.size; // needs to be the short code
- data.details.tier = tiers;
- data.attributes.ac.value = 10 + Math.max(tiers - 1, 0);
- data.attributes.hull.die = sizeData.hullDice;
- data.attributes.hull.dicemax = sizeData.hullDiceStart + tiers;
- data.attributes.hull.dice = sizeData.hullDiceStart + tiers - (parseInt(sizeData.hullDiceUsed) || 0);
- data.attributes.shld.die = sizeData.shldDice;
- data.attributes.shld.dicemax = sizeData.shldDiceStart + tiers;
- data.attributes.shld.dice = sizeData.shldDiceStart + tiers - (parseInt(sizeData.shldDiceUsed) || 0);
- sizeData.pwrDice = SW5E.powerDieTypes[tiers];
- data.attributes.power.die = sizeData.pwrDice;
- data.attributes.cost.baseBuild = sizeData.buildBaseCost;
- data.attributes.workforce.minBuild = sizeData.buildMinWorkforce;
- data.attributes.workforce.max = data.attributes.workforce.minBuild * 5;
- data.attributes.cost.baseUpgrade = SW5E.baseUpgradeCost[tiers];
- data.attributes.cost.multUpgrade = sizeData.upgrdCostMult;
- data.attributes.workforce.minUpgrade = sizeData.upgrdMinWorkforce;
- data.attributes.equip.size.crewMinWorkforce = (parseInt(sizeData.crewMinWorkforce) || 1);
- data.attributes.mods.capLimit = sizeData.modBaseCap;
- data.attributes.mods.suites.cap = sizeData.modMaxSuiteCap;
- data.attributes.cost.multModification = sizeData.modCostMult;
- data.attributes.workforce.minModification = sizeData.modMinWorkforce;
- data.attributes.cost.multEquip = sizeData.equipCostMult;
- data.attributes.workforce.minEquip = sizeData.equipMinWorkforce;
- data.attributes.equip.size.cargoCap = sizeData.cargoCap;
- data.attributes.fuel.cost = sizeData.fuelCost;
- data.attributes.fuel.cap = sizeData.fuelCap;
- data.attributes.equip.size.foodCap = sizeData.foodCap;
- }
-
- // Determine Starship armor-based properties based on owned Starship item
- const armor = actorData.items.filter(i => ((i.type === "equipment") && (i.data.armor.type === "ssarmor"))); // && (i.data.equipped === true)));
- if (armor.length !== 0) {
- const armorData = armor[0].data;
- data.attributes.equip.armor.dr = (parseInt(armorData.dmgred.value) || 0);
- data.attributes.equip.armor.maxDex = armorData.armor.dex;
- data.attributes.equip.armor.stealthDisadv = armorData.stealth;
- }
-
- // Determine Starship hyperdrive-based properties based on owned Starship item
- const hyperdrive = actorData.items.filter(i => ((i.type === "equipment") && (i.data.armor.type === "hyper"))); // && (i.data.equipped === true)));
- if (hyperdrive.length !== 0) {
- const hdData = hyperdrive[0].data;
- data.attributes.equip.hyperdrive.class = (parseFloat(hdData.hdclass.value) || null);
- }
-
- // Determine Starship power coupling-based properties based on owned Starship item
- const pwrcpl = actorData.items.filter(i => ((i.type === "equipment") && (i.data.armor.type === "powerc"))); // && (i.data.equipped === true)));
- if (pwrcpl.length !== 0) {
- const pwrcplData = pwrcpl[0].data;
- data.attributes.equip.powerCoupling.centralCap = (parseInt(pwrcplData.cscap.value) || 0);
- data.attributes.equip.powerCoupling.systemCap = (parseInt(pwrcplData.sscap.value) || 0);
- data.attributes.power.central.max = 0;
- data.attributes.power.comms.max = 0;
- data.attributes.power.engines.max = 0;
- data.attributes.power.shields.max = 0;
- data.attributes.power.sensors.max = 0;
- data.attributes.power.weapons.max = 0;
- }
-
- // Determine Starship reactor-based properties based on owned Starship item
- const reactor = actorData.items.filter(i => ((i.type === "equipment") && (i.data.armor.type === "reactor"))); // && (i.data.equipped === true)));
- if (reactor.length !== 0) {
- const reactorData = reactor[0].data;
- data.attributes.equip.reactor.fuelMult = (parseFloat(reactorData.fuelcostsmod.value) || 0);
- data.attributes.equip.reactor.powerRecDie = reactorData.powdicerec.value;
- }
-
- // Determine Starship shield-based properties based on owned Starship item
- const shields = actorData.items.filter(i => ((i.type === "equipment") && (i.data.armor.type === "ssshield"))); // && (i.data.equipped === true)));
- if (shields.length !== 0) {
- const shieldsData = shields[0].data;
- data.attributes.equip.shields.capMult = (parseFloat(shieldsData.capx.value) || 1);
- data.attributes.equip.shields.regenRateMult = (parseFloat(shieldsData.regrateco.value) || 1);
- }
-
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare skill checks.
- * @param actorData
- * @param bonuses Global bonus data.
- * @param checkBonus Ability check specific bonus.
- * @param originalSkills A transformed actor's original actor's skills.
- * @private
- */
- _prepareSkills(actorData, bonuses, checkBonus, originalSkills) {
- if (actorData.type === 'vehicle') return;
-
- const data = actorData.data;
- const flags = actorData.flags.sw5e || {};
-
- // Skill modifiers
- const feats = SW5E.characterFlags;
- const athlete = flags.remarkableAthlete;
- const joat = flags.jackOfAllTrades;
- const observant = flags.observantFeat;
- const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0;
- for (let [id, skl] of Object.entries(data.skills)) {
- skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0;
- let round = Math.floor;
-
- // Remarkable
- if ( athlete && (skl.value < 0.5) && feats.remarkableAthlete.abilities.includes(skl.ability) ) {
- skl.value = 0.5;
- round = Math.ceil;
- }
-
- // Jack of All Trades
- if ( joat && (skl.value < 0.5) ) {
- skl.value = 0.5;
- }
-
- // Polymorph Skill Proficiencies
- 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(skl.value * data.attributes.prof);
- skl.total = skl.mod + skl.prof + skl.bonus;
-
- // Compute passive bonus
- const passive = observant && (feats.observantFeat.skills.includes(id)) ? 5 : 0;
- skl.passive = 10 + skl.total + passive;
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare data related to the power-casting capabilities of the Actor
- * @private
- */
- _computeBasePowercasting (actorData) {
- if (actorData.type === 'vehicle' || actorData.type === 'starship') return;
- const powers = actorData.data.powers;
- const isNPC = actorData.type === 'npc';
-
- // Translate the list of classes into force and tech power-casting progression
- const forceProgression = {
- classes: 0,
- levels: 0,
- multi: 0,
- maxClass: "none",
- maxClassPriority: 0,
- maxClassLevels: 0,
- maxClassPowerLevel: 0,
- powersKnown: 0,
- points: 0
- };
- const techProgression = {
- classes: 0,
- levels: 0,
- multi: 0,
- maxClass: "none",
- maxClassPriority: 0,
- maxClassLevels: 0,
- maxClassPowerLevel: 0,
- powersKnown: 0,
- points: 0
- };
-
- // Tabulate the total power-casting progression
- const classes = this.data.items.filter(i => i.type === "class");
- let priority = 0;
- for ( let cls of classes ) {
- const d = cls.data;
- if ( d.powercasting === "none" ) continue;
- const levels = d.levels;
- const prog = d.powercasting;
-
- switch (prog) {
- case 'consular':
- priority = 3;
- forceProgression.levels += levels;
- forceProgression.multi += (SW5E.powerMaxLevel['consular'][19]/9)*levels;
- forceProgression.classes++;
- // see if class controls high level forcecasting
- if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
- forceProgression.maxClass = 'consular';
- forceProgression.maxClassLevels = levels;
- forceProgression.maxClassPriority = priority;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['consular'][Math.clamped((levels - 1), 0, 20)];
- }
- // calculate points and powers known
- forceProgression.powersKnown += SW5E.powersKnown['consular'][Math.clamped((levels - 1), 0, 20)];
- forceProgression.points += SW5E.powerPoints['consular'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'engineer':
- priority = 2
- techProgression.levels += levels;
- techProgression.multi += (SW5E.powerMaxLevel['engineer'][19]/9)*levels;
- techProgression.classes++;
- // see if class controls high level techcasting
- if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){
- techProgression.maxClass = 'engineer';
- techProgression.maxClassLevels = levels;
- techProgression.maxClassPriority = priority;
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['engineer'][Math.clamped((levels - 1), 0, 20)];
- }
- techProgression.powersKnown += SW5E.powersKnown['engineer'][Math.clamped((levels - 1), 0, 20)];
- techProgression.points += SW5E.powerPoints['engineer'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'guardian':
- priority = 1;
- forceProgression.levels += levels;
- forceProgression.multi += (SW5E.powerMaxLevel['guardian'][19]/9)*levels;
- forceProgression.classes++;
- // see if class controls high level forcecasting
- if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
- forceProgression.maxClass = 'guardian';
- forceProgression.maxClassLevels = levels;
- forceProgression.maxClassPriority = priority;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['guardian'][Math.clamped((levels - 1), 0, 20)];
- }
- forceProgression.powersKnown += SW5E.powersKnown['guardian'][Math.clamped((levels - 1), 0, 20)];
- forceProgression.points += SW5E.powerPoints['guardian'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'scout':
- priority = 1;
- techProgression.levels += levels;
- techProgression.multi += (SW5E.powerMaxLevel['scout'][19]/9)*levels;
- techProgression.classes++;
- // see if class controls high level techcasting
- if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){
- techProgression.maxClass = 'scout';
- techProgression.maxClassLevels = levels;
- techProgression.maxClassPriority = priority;
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['scout'][Math.clamped((levels - 1), 0, 20)];
- }
- techProgression.powersKnown += SW5E.powersKnown['scout'][Math.clamped((levels - 1), 0, 20)];
- techProgression.points += SW5E.powerPoints['scout'][Math.clamped((levels - 1), 0, 20)];
- break;
- case 'sentinel':
- priority = 2;
- forceProgression.levels += levels;
- forceProgression.multi += (SW5E.powerMaxLevel['sentinel'][19]/9)*levels;
- forceProgression.classes++;
- // see if class controls high level forcecasting
- if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
- forceProgression.maxClass = 'sentinel';
- forceProgression.maxClassLevels = levels;
- forceProgression.maxClassPriority = priority;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['sentinel'][Math.clamped((levels - 1), 0, 20)];
- }
- forceProgression.powersKnown += SW5E.powersKnown['sentinel'][Math.clamped((levels - 1), 0, 20)];
- forceProgression.points += SW5E.powerPoints['sentinel'][Math.clamped((levels - 1), 0, 20)];
- break; }
- }
-
- // EXCEPTION: multi-classed progression uses multi rounded down rather than levels
- if (!isNPC && forceProgression.classes > 1) {
- forceProgression.levels = Math.floor(forceProgression.multi);
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][forceProgression.levels - 1];
- }
- if (!isNPC && techProgression.classes > 1) {
- techProgression.levels = Math.floor(techProgression.multi);
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][techProgression.levels - 1];
- }
-
- // EXCEPTION: NPC with an explicit power-caster level
- if (isNPC && actorData.data.details.powerForceLevel) {
- forceProgression.levels = actorData.data.details.powerForceLevel;
- actorData.data.attributes.force.level = forceProgression.levels;
- forceProgression.maxClass = actorData.data.attributes.powercasting;
- forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped((forceProgression.levels - 1), 0, 20)];
- }
- if (isNPC && actorData.data.details.powerTechLevel) {
- techProgression.levels = actorData.data.details.powerTechLevel;
- actorData.data.attributes.tech.level = techProgression.levels;
- techProgression.maxClass = actorData.data.attributes.powercasting;
- techProgression.maxClassPowerLevel = SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped((techProgression.levels - 1), 0, 20)];
- }
-
- // Look up the number of slots per level from the powerLimit table
- let forcePowerLimit = Array.from(SW5E.powerLimit['none']);
- for (let i = 0; i < (forceProgression.maxClassPowerLevel); i++) {
- forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i];
- }
-
- for ( let [n, lvl] of Object.entries(powers) ) {
- let i = parseInt(n.slice(-1));
- if ( Number.isNaN(i) ) continue;
- if ( Number.isNumeric(lvl.foverride) ) lvl.fmax = Math.max(parseInt(lvl.foverride), 0);
- else lvl.fmax = forcePowerLimit[i-1] || 0;
- if (isNPC){
- lvl.fvalue = lvl.fmax;
- }else{
- lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax),lvl.fmax);
- }
- }
-
- let techPowerLimit = Array.from(SW5E.powerLimit['none']);
- for (let i = 0; i < (techProgression.maxClassPowerLevel); i++) {
- techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i];
- }
-
- for ( let [n, lvl] of Object.entries(powers) ) {
- let i = parseInt(n.slice(-1));
- if ( Number.isNaN(i) ) continue;
- if ( Number.isNumeric(lvl.toverride) ) lvl.tmax = Math.max(parseInt(lvl.toverride), 0);
- else lvl.tmax = techPowerLimit[i-1] || 0;
- if (isNPC){
- lvl.tvalue = lvl.tmax;
- }else{
- lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax),lvl.tmax);
- }
- }
-
- // Set Force and tech power for PC Actors
- if (!isNPC && forceProgression.levels){
- actorData.data.attributes.force.known.max = forceProgression.powersKnown;
- actorData.data.attributes.force.points.max = forceProgression.points; // + Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod);
- actorData.data.attributes.force.level = forceProgression.levels;
- }
- if (!isNPC && techProgression.levels){
- actorData.data.attributes.tech.known.max = techProgression.powersKnown;
- actorData.data.attributes.tech.points.max = techProgression.points; // + actorData.data.abilities.int.mod;
- actorData.data.attributes.tech.level = techProgression.levels;
- }
-
- // Tally Powers Known and check for migration first to avoid errors
- let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined;
- if ( hasKnownPowers ) {
- const knownPowers = this.data.items.filter(i => i.type === "power");
- let knownForcePowers = 0;
- let knownTechPowers = 0;
- for ( let knownPower of knownPowers ) {
- const d = knownPower.data;
- switch (knownPower.data.school){
- case "lgt":
- case "uni":
- case "drk":{
- knownForcePowers++;
- break;
- }
- case "tec":{
- knownTechPowers++;
- break;
- }
- }
- continue;
- }
- actorData.data.attributes.force.known.value = knownForcePowers;
- actorData.data.attributes.tech.known.value = knownTechPowers;
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare data related to the power-casting capabilities of the Actor
- * @private
- */
- _computeDerivedPowercasting (actorData) {
- if (actorData.type !== 'actor') return;
-
- // Set Force and tech power for PC Actors
- if (!!actorData.data.attributes.force.level){
- actorData.data.attributes.force.points.max += Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod);
- }
- if (!!actorData.data.attributes.tech.level){
- actorData.data.attributes.tech.points.max += actorData.data.abilities.int.mod;
- }
-
- }
-
- /* -------------------------------------------- */
-
- /**
- * Compute the level and percentage of encumbrance for an Actor.
- *
- * Optionally include the weight of carried currency across all denominations by applying the standard rule
- * from the PHB pg. 143
- * @param {Object} actorData The data object for the Actor being rendered
- * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level
- * @private
- */
- _computeEncumbrance(actorData) {
-
- // Get the total weight from items
- const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"];
- let weight = actorData.items.reduce((weight, i) => {
- if ( !physicalItems.includes(i.type) ) return weight;
- const q = i.data.quantity || 0;
- const w = i.data.weight || 0;
- return weight + (q * w);
- }, 0);
-
- // [Optional] add Currency Weight (for non-transformed actors)
- if ( game.settings.get("sw5e", "currencyWeight") && actorData.data.currency ) {
- const currency = actorData.data.currency;
- const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0);
- weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
- }
-
- // Determine the encumbrance size class
- let mod = {
- tiny: 0.5,
- sm: 1,
- med: 1,
- lg: 2,
- huge: 4,
- grg: 8
- }[actorData.data.traits.size] || 1;
- 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.toNearest(0.1), max, pct, encumbered: pct > (2/3) };
- }
-
- _computeFuel(actorData) {
- const fuel = actorData.data.attributes.fuel;
- // Compute Fuel percentage
- const pct = Math.clamped((fuel.value.toNearest(0.1) * 100) / fuel.cap, 0, 100);
- return { ...fuel, pct, fueled: pct > 0 };
- }
-
/* -------------------------------------------- */
- /* Socket Listeners and Handlers
- /* -------------------------------------------- */
- /** @override */
- static async create(data, options={}) {
- data.token = data.token || {};
- if ( data.type === "character" ) {
- mergeObject(data.token, {
- vision: true,
- dimSight: 30,
- brightSight: 0,
- actorLink: true,
- disposition: 1
- }, {overwrite: false});
- }
- return super.create(data, options);
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async update(data, options={}) {
-
- // 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")) ) {
- let size = CONFIG.SW5E.tokenSizes[newSize];
- if ( this.isToken ) this.token.update({height: size, width: size});
- else if ( !data["token.width"] && !hasProperty(data, "token.width") ) {
- data["token.height"] = size;
- 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);
- setProperty(data, "data.attributes.death.failure", 0);
- }
-
- // Perform the update
- return super.update(data, options);
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async createEmbeddedEntity(embeddedName, itemData, options={}) {
-
- // Pre-creation steps for owned items
- if ( embeddedName === "OwnedItem" ) this._preCreateOwnedItem(itemData, options);
-
- // Standard embedded entity creation
- return super.createEmbeddedEntity(embeddedName, itemData, options);
- }
-
- /* -------------------------------------------- */
-
- /**
- * A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer
- * @param itemData
- * @param options
- * @private
- */
- _preCreateOwnedItem(itemData, options) {
- if ( this.data.type === "vehicle" ) return;
- const isNPC = this.data.type === 'npc';
- let initial = {};
- switch ( itemData.type ) {
-
- case "weapon":
- if ( getProperty(itemData, "data.equipped") === undefined ) {
- initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
+ /** @override */
+ prepareBaseData() {
+ switch (this.data.type) {
+ case "character":
+ return this._prepareCharacterData(this.data);
+ case "npc":
+ return this._prepareNPCData(this.data);
+ case "starship":
+ return this._prepareStarshipData(this.data);
+ case "vehicle":
+ return this._prepareVehicleData(this.data);
}
- if ( getProperty(itemData, "data.proficient") === undefined ) {
- if ( isNPC ) {
- initial["data.proficient"] = true; // NPCs automatically have equipment proficiency
- } else {
- const weaponProf = {
- "natural": true,
- "simpleVW": "sim",
- "simpleB": "sim",
- "simpleLW": "sim",
- "martialVW": "mar",
- "martialB": "mar",
- "martialLW": "mar"
- }[itemData.data?.weaponType]; // Player characters check proficiency
- const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
- const hasWeaponProf = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
- initial["data.proficient"] = hasWeaponProf;
- }
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ prepareDerivedData() {
+ const actorData = this.data;
+ const data = actorData.data;
+ const flags = actorData.flags.sw5e || {};
+ const bonuses = getProperty(data, "bonuses.abilities") || {};
+
+ // Retrieve data for polymorphed actors
+ let originalSaves = null;
+ let originalSkills = null;
+ if (this.isPolymorphed) {
+ const transformOptions = this.getFlag("sw5e", "transformOptions");
+ const original = game.actors?.get(this.getFlag("sw5e", "originalActor"));
+ if (original) {
+ if (transformOptions.mergeSaves) {
+ originalSaves = original.data.data.abilities;
+ }
+ if (transformOptions.mergeSkills) {
+ originalSkills = original.data.data.skills;
+ }
+ }
}
- break;
- case "equipment":
- if ( getProperty(itemData, "data.equipped") === undefined ) {
- initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
+ // 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)) {
+ abl.mod = Math.floor((abl.value - 10) / 2);
+ abl.prof = (abl.proficient || 0) * data.attributes.prof;
+ abl.saveBonus = saveBonus;
+ abl.checkBonus = checkBonus;
+ abl.save = abl.mod + abl.prof + abl.saveBonus;
+ abl.dc = 8 + abl.mod + data.attributes.prof + dcBonus;
+
+ // If we merged saves when transforming, take the highest bonus here.
+ if (originalSaves && abl.proficient) {
+ abl.save = Math.max(abl.save, originalSaves[id].save);
+ }
}
- if ( getProperty(itemData, "data.proficient") === undefined ) {
- if ( isNPC ) {
- initial["data.proficient"] = true; // NPCs automatically have equipment proficiency
- } else {
- const armorProf = {
- "natural": true,
- "clothing": true,
- "light": "lgt",
- "medium": "med",
- "heavy": "hvy",
- "shield": "shl"
- }[itemData.data?.armor?.type]; // Player characters check proficiency
- const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
- const hasEquipmentProf = (armorProf === true) || actorArmorProfs.includes(armorProf);
- initial["data.proficient"] = hasEquipmentProf;
- }
+
+ // Inventory encumbrance
+ data.attributes.encumbrance = this._computeEncumbrance(actorData);
+
+ if (actorData.type === "starship") {
+ // Calculate AC
+ data.attributes.ac.value += Math.min(data.abilities.dex.mod, data.attributes.equip.armor.maxDex);
+
+ // Set Power Die Storage
+ data.attributes.power.central.max += data.attributes.equip.powerCoupling.centralCap;
+ data.attributes.power.comms.max += data.attributes.equip.powerCoupling.systemCap;
+ data.attributes.power.engines.max += data.attributes.equip.powerCoupling.systemCap;
+ data.attributes.power.shields.max += data.attributes.equip.powerCoupling.systemCap;
+ data.attributes.power.sensors.max += data.attributes.equip.powerCoupling.systemCap;
+ data.attributes.power.weapons.max += data.attributes.equip.powerCoupling.systemCap;
+
+ // Find Size info of Starship
+ const size = actorData.items.filter((i) => i.type === "starship");
+ if (size.length === 0) return;
+ const sizeData = size[0].data;
+
+ // Prepare Hull Points
+ data.attributes.hp.max =
+ sizeData.hullDiceRolled.reduce((a, b) => a + b, 0) +
+ data.abilities.con.mod * data.attributes.hull.dicemax;
+ if (data.attributes.hp.value === null) data.attributes.hp.value = data.attributes.hp.max;
+
+ // Prepare Shield Points
+ data.attributes.hp.tempmax =
+ (sizeData.shldDiceRolled.reduce((a, b) => a + b, 0) +
+ data.abilities.str.mod * data.attributes.shld.dicemax) *
+ data.attributes.equip.shields.capMult;
+ if (data.attributes.hp.temp === null) data.attributes.hp.temp = data.attributes.hp.tempmax;
+
+ // Prepare Speeds
+ data.attributes.movement.space =
+ sizeData.baseSpaceSpeed + 50 * (data.abilities.str.mod - data.abilities.con.mod);
+ data.attributes.movement.turn = Math.min(
+ data.attributes.movement.space,
+ Math.max(50, sizeData.baseTurnSpeed - 50 * (data.abilities.dex.mod - data.abilities.con.mod))
+ );
+
+ // Prepare Max Suites
+ data.attributes.mods.suites.max =
+ sizeData.modMaxSuitesBase + sizeData.modMaxSuitesMult * data.abilities.con.mod;
+
+ // Prepare Hardpoints
+ data.attributes.mods.hardpoints.max = sizeData.hardpointMult * Math.max(1, data.abilities.str.mod);
+
+ //Prepare Fuel
+ data.attributes.fuel = this._computeFuel(actorData);
}
- break;
- case "power":
- initial["data.prepared"] = true; // automatically prepare powers for everyone
- break;
- }
- mergeObject(itemData, initial);
- }
+ // Prepare skills
+ this._prepareSkills(actorData, bonuses, checkBonus, originalSkills);
- /* -------------------------------------------- */
- /* Gameplay Mechanics */
- /* -------------------------------------------- */
+ // Determine Initiative Modifier
+ const init = data.attributes.init;
+ const athlete = flags.remarkableAthlete;
+ const joat = flags.jackOfAllTrades;
+ init.mod = data.abilities.dex.mod;
+ 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.value = init.value ?? 0;
+ init.bonus = init.value + (flags.initiativeAlert ? 5 : 0);
+ init.total = init.mod + init.prof + init.bonus;
- /** @override */
- async modifyTokenAttribute(attribute, value, isDelta, isBar) {
- if ( attribute === "attributes.hp" ) {
- const hp = getProperty(this.data.data, attribute);
- const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value;
- return this.applyDamage(delta);
- }
- return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
- }
+ // Prepare power-casting data
+ data.attributes.powerForceLightDC = 8 + data.abilities.wis.mod + data.attributes.prof ?? 10;
+ data.attributes.powerForceDarkDC = 8 + data.abilities.cha.mod + data.attributes.prof ?? 10;
+ data.attributes.powerForceUnivDC =
+ Math.max(data.attributes.powerForceLightDC, data.attributes.powerForceDarkDC) ?? 10;
+ data.attributes.powerTechDC = 8 + data.abilities.int.mod + data.attributes.prof ?? 10;
+ this._computeDerivedPowercasting(this.data);
- /* -------------------------------------------- */
-
- /**
- * Apply a certain amount of damage or healing to the health pool for Actor
- * @param {number} amount An amount of damage (positive) or healing (negative) to sustain
- * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing
- * @return {Promise} A Promise which resolves once the damage has been applied
- */
- async applyDamage(amount=0, multiplier=1) {
- amount = Math.floor(parseInt(amount) * multiplier);
- const hp = this.data.data.attributes.hp;
-
- // Deduct damage from temp HP first
- const tmp = parseInt(hp.temp) || 0;
- const dt = amount > 0 ? Math.min(tmp, amount) : 0;
-
- // Remaining goes to health
- const tmpMax = parseInt(hp.tempmax) || 0;
- const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax);
-
- // Update the Actor
- const updates = {
- "data.attributes.hp.temp": tmp - dt,
- "data.attributes.hp.value": dh
- };
-
- // Delegate damage application to a hook
- // TODO replace this in the future with a better modifyTokenAttribute function in the core
- const allowed = Hooks.call("modifyTokenAttribute", {
- attribute: "attributes.hp",
- value: amount,
- isDelta: false,
- isBar: true
- }, updates);
- return allowed !== false ? this.update(updates) : this;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll a Skill Check
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {string} skillId The skill id (e.g. "ins")
- * @param {Object} options Options which configure how the skill check is rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollSkill(skillId, options={}) {
- const skl = this.data.data.skills[skillId];
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
-
- // Compose roll parts and data
- const parts = ["@mod"];
- const data = {mod: skl.mod + skl.prof};
-
- // Ability test bonus
- if ( bonuses.check ) {
- data["checkBonus"] = bonuses.check;
- parts.push("@checkBonus");
- }
-
- // Skill check bonus
- if ( bonuses.skill ) {
- data["skillBonus"] = bonuses.skill;
- parts.push("@skillBonus");
- }
-
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
- }
-
- // Reliable Talent applies to any skill check we have full or better proficiency in
- const reliableTalent = (skl.value >= 1 && this.getFlag("sw5e", "reliableTalent"));
-
- // Roll and return
- const rollData = mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.format("SW5E.SkillPromptTitle", {skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]}),
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- reliableTalent: reliableTalent,
- messageData: {"flags.sw5e.roll": {type: "skill", skillId }}
- });
- rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
- return d20Roll(rollData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll a generic ability test or saving throw.
- * Prompt the user for input on which variety of roll they want to do.
- * @param {String}abilityId The ability id (e.g. "str")
- * @param {Object} options Options which configure how ability tests or saving throws are rolled
- */
- rollAbility(abilityId, options={}) {
- const label = CONFIG.SW5E.abilities[abilityId];
- new Dialog({
- title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
- content: `${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}
`,
- buttons: {
- test: {
- label: game.i18n.localize("SW5E.ActionAbil"),
- callback: () => this.rollAbilityTest(abilityId, options)
- },
- save: {
- label: game.i18n.localize("SW5E.ActionSave"),
- callback: () => this.rollAbilitySave(abilityId, options)
- }
- }
- }).render(true);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll an Ability Test
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {String} abilityId The ability ID (e.g. "str")
- * @param {Object} options Options which configure how ability tests are rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollAbilityTest(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};
-
- // Add feat-related proficiency bonuses
- const feats = this.data.flags.sw5e || {};
- if ( feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId) ) {
- parts.push("@proficiency");
- data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof);
- }
- else if ( feats.jackOfAllTrades ) {
- parts.push("@proficiency");
- data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof);
- }
-
- // Add global actor bonus
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
- if ( bonuses.check ) {
- parts.push("@checkBonus");
- data.checkBonus = bonuses.check;
- }
-
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
- }
-
- // Roll and return
- const rollData = mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
- halflingLucky: feats.halflingLucky,
- messageData: {"flags.sw5e.roll": {type: "ability", abilityId }}
- });
- rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
- return d20Roll(rollData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll an Ability Saving Throw
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {String} abilityId The ability ID (e.g. "str")
- * @param {Object} options Options which configure how ability tests are rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- 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};
-
- // Include proficiency bonus
- if ( abl.prof > 0 ) {
- parts.push("@prof");
- data.prof = abl.prof;
- }
-
- // Include a global actor ability save bonus
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
- if ( bonuses.save ) {
- parts.push("@saveBonus");
- data.saveBonus = bonuses.save;
- }
-
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
- }
-
- // Roll and return
- const rollData = mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}),
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- messageData: {"flags.sw5e.roll": {type: "save", abilityId }}
- });
- rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
- return d20Roll(rollData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Perform a death saving throw, rolling a d20 plus any global save bonuses
- * @param {Object} options Additional options which modify the roll
- * @return {Promise} A Promise which resolves to the Roll instance
- */
- async rollDeathSave(options={}) {
-
- // Display a warning if we are not at zero HP or if we already have reached 3
- const death = this.data.data.attributes.death;
- if ( (this.data.data.attributes.hp.value > 0) || (death.failure >= 3) || (death.success >= 3)) {
- ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary"));
- return null;
- }
-
- // Evaluate a global saving throw bonus
- const parts = [];
- const data = {};
- const speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
-
- // Include a global actor ability save bonus
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
- if ( bonuses.save ) {
- parts.push("@saveBonus");
- data.saveBonus = bonuses.save;
- }
-
- // Evaluate the roll
- const rollData = mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.localize("SW5E.DeathSavingThrow"),
- speaker: speaker,
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- targetValue: 10,
- messageData: {"flags.sw5e.roll": {type: "death"}}
- });
- rollData.speaker = speaker;
- const roll = await d20Roll(rollData);
- if ( !roll ) return null;
-
- // Take action depending on the result
- const success = roll.total >= 10;
- const d20 = roll.dice[0].total;
-
- // Save success
- if ( success ) {
- let successes = (death.success || 0) + 1;
-
- // Critical Success = revive with 1hp
- if ( d20 === 20 ) {
- await this.update({
- "data.attributes.death.success": 0,
- "data.attributes.death.failure": 0,
- "data.attributes.hp.value": 1
+ // Compute owned item attributes which depend on prepared Actor data
+ this.items.forEach((item) => {
+ item.getSaveDC();
+ item.getAttackToHit();
});
- await ChatMessage.create({content: game.i18n.format("SW5E.DeathSaveCriticalSuccess", {name: this.name}), speaker});
- }
+ }
- // 3 Successes = survive and reset checks
- else if ( successes === 3 ) {
- await this.update({
- "data.attributes.death.success": 0,
- "data.attributes.death.failure": 0
+ /* -------------------------------------------- */
+
+ /**
+ * Return the amount of experience required to gain a certain character level.
+ * @param level {Number} The desired level
+ * @return {Number} The XP required
+ */
+ getLevelExp(level) {
+ const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS;
+ return levels[Math.min(level, levels.length - 1)];
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Return the amount of experience granted by killing a creature of a certain CR.
+ * @param cr {Number} The creature's challenge rating
+ * @return {Number} The amount of experience granted per kill
+ */
+ getCRExp(cr) {
+ if (cr < 1.0) return Math.max(200 * cr, 10);
+ return CONFIG.SW5E.CR_EXP_LEVELS[cr];
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getRollData() {
+ const data = super.getRollData();
+ data.classes = this.data.items.reduce((obj, i) => {
+ if (i.type === "class") {
+ obj[i.name.slugify({strict: true})] = i.data;
+ }
+ return obj;
+ }, {});
+ data.prof = this.data.data.attributes.prof || 0;
+ return data;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Return the features which a character is awarded for each class level
+ * @param {string} className The class name being added
+ * @param {string} archetypeName The archetype 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} Array of Item5e entities
+ */
+ static async getClassFeatures({className = "", archetypeName = "", level = 1, priorLevel = 0} = {}) {
+ className = className.toLowerCase();
+ archetypeName = archetypeName.slugify();
+
+ // Get the configuration of features which may be added
+ const clsConfig = CONFIG.SW5E.classFeatures[className];
+ if (!clsConfig) return [];
+
+ // 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 archetype features
+ const archConfig = clsConfig.archetypes[archetypeName] || {};
+ for (let [l, f] of Object.entries(archConfig.features || {})) {
+ l = parseInt(l);
+ if (l <= level && l > priorLevel) ids = ids.concat(f);
+ }
+
+ // Load item data for all identified features
+ const features = [];
+ for (let id of ids) {
+ features.push(await 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 updateData = expandObject(u);
+ const config = {
+ className: updateData.name || item.data.name,
+ archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype,
+ level: getProperty(updateData, "data.levels"),
+ priorLevel: item ? item.data.data.levels : 0
+ };
+
+ // Get and create features for an increased class level
+ let changed = false;
+ if (config.level && config.level > config.priorLevel) changed = true;
+ if (config.archetypeName !== item.data.data.archetype) changed = true;
+
+ // Get features to create
+ if (changed) {
+ 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);
+ }
+ }
+ }
+ return toCreate;
+ }
+
+ /* -------------------------------------------- */
+ /* Data Preparation Helpers */
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare Character type specific data
+ */
+ _prepareCharacterData(actorData) {
+ const data = actorData.data;
+
+ // Determine character level and available hit dice based on owned Class items
+ const [level, hd] = actorData.items.reduce(
+ (arr, item) => {
+ if (item.type === "class") {
+ const classLevels = parseInt(item.data.levels) || 1;
+ arr[0] += classLevels;
+ arr[1] += classLevels - (parseInt(item.data.hitDiceUsed) || 0);
+ }
+ return arr;
+ },
+ [0, 0]
+ );
+ data.details.level = level;
+ data.attributes.hd = hd;
+
+ // Character proficiency bonus
+ data.attributes.prof = Math.floor((level + 7) / 4);
+
+ // Experience required for next level
+ const xp = data.details.xp;
+ xp.max = this.getLevelExp(level || 1);
+ const prior = this.getLevelExp(level - 1 || 0);
+ const required = xp.max - prior;
+ const pct = Math.round(((xp.value - prior) * 100) / required);
+ xp.pct = Math.clamped(pct, 0, 100);
+
+ // Add base Powercasting attributes
+ this._computeBasePowercasting(actorData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare NPC type specific data
+ */
+ _prepareNPCData(actorData) {
+ const data = actorData.data;
+
+ // Kill Experience
+ data.details.xp.value = this.getCRExp(data.details.cr);
+
+ // Proficiency
+ data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4);
+
+ this._computeBasePowercasting(actorData);
+
+ // Powercaster Level
+ if (data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel)) {
+ data.details.powerLevel = Math.max(data.details.cr, 1);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare vehicle type-specific data
+ * @param actorData
+ * @private
+ */
+ _prepareVehicleData(actorData) {}
+
+ /* -------------------------------------------- */
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare starship type-specific data
+ * @param actorData
+ * @private
+ */
+ _prepareStarshipData(actorData) {
+ const data = actorData.data;
+ data.attributes.prof = 0;
+ // Determine Starship size-based properties based on owned Starship item
+ const size = actorData.items.filter((i) => i.type === "starship");
+ if (size.length !== 0) {
+ const sizeData = size[0].data;
+ const tiers = parseInt(sizeData.tier) || 0;
+ data.traits.size = sizeData.size; // needs to be the short code
+ data.details.tier = tiers;
+ data.attributes.ac.value = 10 + Math.max(tiers - 1, 0);
+ data.attributes.hull.die = sizeData.hullDice;
+ data.attributes.hull.dicemax = sizeData.hullDiceStart + tiers;
+ data.attributes.hull.dice = sizeData.hullDiceStart + tiers - (parseInt(sizeData.hullDiceUsed) || 0);
+ data.attributes.shld.die = sizeData.shldDice;
+ data.attributes.shld.dicemax = sizeData.shldDiceStart + tiers;
+ data.attributes.shld.dice = sizeData.shldDiceStart + tiers - (parseInt(sizeData.shldDiceUsed) || 0);
+ sizeData.pwrDice = SW5E.powerDieTypes[tiers];
+ data.attributes.power.die = sizeData.pwrDice;
+ data.attributes.cost.baseBuild = sizeData.buildBaseCost;
+ data.attributes.workforce.minBuild = sizeData.buildMinWorkforce;
+ data.attributes.workforce.max = data.attributes.workforce.minBuild * 5;
+ data.attributes.cost.baseUpgrade = SW5E.baseUpgradeCost[tiers];
+ data.attributes.cost.multUpgrade = sizeData.upgrdCostMult;
+ data.attributes.workforce.minUpgrade = sizeData.upgrdMinWorkforce;
+ data.attributes.equip.size.crewMinWorkforce = parseInt(sizeData.crewMinWorkforce) || 1;
+ data.attributes.mods.capLimit = sizeData.modBaseCap;
+ data.attributes.mods.suites.cap = sizeData.modMaxSuiteCap;
+ data.attributes.cost.multModification = sizeData.modCostMult;
+ data.attributes.workforce.minModification = sizeData.modMinWorkforce;
+ data.attributes.cost.multEquip = sizeData.equipCostMult;
+ data.attributes.workforce.minEquip = sizeData.equipMinWorkforce;
+ data.attributes.equip.size.cargoCap = sizeData.cargoCap;
+ data.attributes.fuel.cost = sizeData.fuelCost;
+ data.attributes.fuel.cap = sizeData.fuelCap;
+ data.attributes.equip.size.foodCap = sizeData.foodCap;
+ }
+
+ // Determine Starship armor-based properties based on owned Starship item
+ const armor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssarmor"); // && (i.data.equipped === true)));
+ if (armor.length !== 0) {
+ const armorData = armor[0].data;
+ data.attributes.equip.armor.dr = parseInt(armorData.dmgred.value) || 0;
+ data.attributes.equip.armor.maxDex = armorData.armor.dex;
+ data.attributes.equip.armor.stealthDisadv = armorData.stealth;
+ }
+
+ // Determine Starship hyperdrive-based properties based on owned Starship item
+ const hyperdrive = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "hyper"); // && (i.data.equipped === true)));
+ if (hyperdrive.length !== 0) {
+ const hdData = hyperdrive[0].data;
+ data.attributes.equip.hyperdrive.class = parseFloat(hdData.hdclass.value) || null;
+ }
+
+ // Determine Starship power coupling-based properties based on owned Starship item
+ const pwrcpl = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "powerc"); // && (i.data.equipped === true)));
+ if (pwrcpl.length !== 0) {
+ const pwrcplData = pwrcpl[0].data;
+ data.attributes.equip.powerCoupling.centralCap = parseInt(pwrcplData.cscap.value) || 0;
+ data.attributes.equip.powerCoupling.systemCap = parseInt(pwrcplData.sscap.value) || 0;
+ data.attributes.power.central.max = 0;
+ data.attributes.power.comms.max = 0;
+ data.attributes.power.engines.max = 0;
+ data.attributes.power.shields.max = 0;
+ data.attributes.power.sensors.max = 0;
+ data.attributes.power.weapons.max = 0;
+ }
+
+ // Determine Starship reactor-based properties based on owned Starship item
+ const reactor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "reactor"); // && (i.data.equipped === true)));
+ if (reactor.length !== 0) {
+ const reactorData = reactor[0].data;
+ data.attributes.equip.reactor.fuelMult = parseFloat(reactorData.fuelcostsmod.value) || 0;
+ data.attributes.equip.reactor.powerRecDie = reactorData.powdicerec.value;
+ }
+
+ // Determine Starship shield-based properties based on owned Starship item
+ const shields = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssshield"); // && (i.data.equipped === true)));
+ if (shields.length !== 0) {
+ const shieldsData = shields[0].data;
+ data.attributes.equip.shields.capMult = parseFloat(shieldsData.capx.value) || 1;
+ data.attributes.equip.shields.regenRateMult = parseFloat(shieldsData.regrateco.value) || 1;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare skill checks.
+ * @param actorData
+ * @param bonuses Global bonus data.
+ * @param checkBonus Ability check specific bonus.
+ * @param originalSkills A transformed actor's original actor's skills.
+ * @private
+ */
+ _prepareSkills(actorData, bonuses, checkBonus, originalSkills) {
+ if (actorData.type === "vehicle") return;
+
+ const data = actorData.data;
+ const flags = actorData.flags.sw5e || {};
+
+ // Skill modifiers
+ const feats = SW5E.characterFlags;
+ const athlete = flags.remarkableAthlete;
+ const joat = flags.jackOfAllTrades;
+ const observant = flags.observantFeat;
+ const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0;
+ for (let [id, skl] of Object.entries(data.skills)) {
+ skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0;
+ let round = Math.floor;
+
+ // Remarkable
+ if (athlete && skl.value < 0.5 && feats.remarkableAthlete.abilities.includes(skl.ability)) {
+ skl.value = 0.5;
+ round = Math.ceil;
+ }
+
+ // Jack of All Trades
+ if (joat && skl.value < 0.5) {
+ skl.value = 0.5;
+ }
+
+ // Polymorph Skill Proficiencies
+ 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(skl.value * data.attributes.prof);
+ skl.total = skl.mod + skl.prof + skl.bonus;
+
+ // Compute passive bonus
+ const passive = observant && feats.observantFeat.skills.includes(id) ? 5 : 0;
+ skl.passive = 10 + skl.total + passive;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare data related to the power-casting capabilities of the Actor
+ * @private
+ */
+ _computeBasePowercasting(actorData) {
+ if (actorData.type === "vehicle" || actorData.type === "starship") return;
+ const powers = actorData.data.powers;
+ const isNPC = actorData.type === "npc";
+
+ // Translate the list of classes into force and tech power-casting progression
+ const forceProgression = {
+ classes: 0,
+ levels: 0,
+ multi: 0,
+ maxClass: "none",
+ maxClassPriority: 0,
+ maxClassLevels: 0,
+ maxClassPowerLevel: 0,
+ powersKnown: 0,
+ points: 0
+ };
+ const techProgression = {
+ classes: 0,
+ levels: 0,
+ multi: 0,
+ maxClass: "none",
+ maxClassPriority: 0,
+ maxClassLevels: 0,
+ maxClassPowerLevel: 0,
+ powersKnown: 0,
+ points: 0
+ };
+
+ // Tabulate the total power-casting progression
+ const classes = this.data.items.filter((i) => i.type === "class");
+ let priority = 0;
+ for (let cls of classes) {
+ const d = cls.data;
+ if (d.powercasting === "none") continue;
+ const levels = d.levels;
+ const prog = d.powercasting;
+
+ switch (prog) {
+ case "consular":
+ priority = 3;
+ forceProgression.levels += levels;
+ forceProgression.multi += (SW5E.powerMaxLevel["consular"][19] / 9) * levels;
+ forceProgression.classes++;
+ // see if class controls high level forcecasting
+ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
+ forceProgression.maxClass = "consular";
+ forceProgression.maxClassLevels = levels;
+ forceProgression.maxClassPriority = priority;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["consular"][Math.clamped(levels - 1, 0, 20)];
+ }
+ // calculate points and powers known
+ forceProgression.powersKnown += SW5E.powersKnown["consular"][Math.clamped(levels - 1, 0, 20)];
+ forceProgression.points += SW5E.powerPoints["consular"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "engineer":
+ priority = 2;
+ techProgression.levels += levels;
+ techProgression.multi += (SW5E.powerMaxLevel["engineer"][19] / 9) * levels;
+ techProgression.classes++;
+ // see if class controls high level techcasting
+ if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) {
+ techProgression.maxClass = "engineer";
+ techProgression.maxClassLevels = levels;
+ techProgression.maxClassPriority = priority;
+ techProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["engineer"][Math.clamped(levels - 1, 0, 20)];
+ }
+ techProgression.powersKnown += SW5E.powersKnown["engineer"][Math.clamped(levels - 1, 0, 20)];
+ techProgression.points += SW5E.powerPoints["engineer"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "guardian":
+ priority = 1;
+ forceProgression.levels += levels;
+ forceProgression.multi += (SW5E.powerMaxLevel["guardian"][19] / 9) * levels;
+ forceProgression.classes++;
+ // see if class controls high level forcecasting
+ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
+ forceProgression.maxClass = "guardian";
+ forceProgression.maxClassLevels = levels;
+ forceProgression.maxClassPriority = priority;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["guardian"][Math.clamped(levels - 1, 0, 20)];
+ }
+ forceProgression.powersKnown += SW5E.powersKnown["guardian"][Math.clamped(levels - 1, 0, 20)];
+ forceProgression.points += SW5E.powerPoints["guardian"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "scout":
+ priority = 1;
+ techProgression.levels += levels;
+ techProgression.multi += (SW5E.powerMaxLevel["scout"][19] / 9) * levels;
+ techProgression.classes++;
+ // see if class controls high level techcasting
+ if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) {
+ techProgression.maxClass = "scout";
+ techProgression.maxClassLevels = levels;
+ techProgression.maxClassPriority = priority;
+ techProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["scout"][Math.clamped(levels - 1, 0, 20)];
+ }
+ techProgression.powersKnown += SW5E.powersKnown["scout"][Math.clamped(levels - 1, 0, 20)];
+ techProgression.points += SW5E.powerPoints["scout"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ case "sentinel":
+ priority = 2;
+ forceProgression.levels += levels;
+ forceProgression.multi += (SW5E.powerMaxLevel["sentinel"][19] / 9) * levels;
+ forceProgression.classes++;
+ // see if class controls high level forcecasting
+ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
+ forceProgression.maxClass = "sentinel";
+ forceProgression.maxClassLevels = levels;
+ forceProgression.maxClassPriority = priority;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel["sentinel"][Math.clamped(levels - 1, 0, 20)];
+ }
+ forceProgression.powersKnown += SW5E.powersKnown["sentinel"][Math.clamped(levels - 1, 0, 20)];
+ forceProgression.points += SW5E.powerPoints["sentinel"][Math.clamped(levels - 1, 0, 20)];
+ break;
+ }
+ }
+
+ // EXCEPTION: multi-classed progression uses multi rounded down rather than levels
+ if (!isNPC && forceProgression.classes > 1) {
+ forceProgression.levels = Math.floor(forceProgression.multi);
+ forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][forceProgression.levels - 1];
+ }
+ if (!isNPC && techProgression.classes > 1) {
+ techProgression.levels = Math.floor(techProgression.multi);
+ techProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][techProgression.levels - 1];
+ }
+
+ // EXCEPTION: NPC with an explicit power-caster level
+ if (isNPC && actorData.data.details.powerForceLevel) {
+ forceProgression.levels = actorData.data.details.powerForceLevel;
+ actorData.data.attributes.force.level = forceProgression.levels;
+ forceProgression.maxClass = actorData.data.attributes.powercasting;
+ forceProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped(forceProgression.levels - 1, 0, 20)];
+ }
+ if (isNPC && actorData.data.details.powerTechLevel) {
+ techProgression.levels = actorData.data.details.powerTechLevel;
+ actorData.data.attributes.tech.level = techProgression.levels;
+ techProgression.maxClass = actorData.data.attributes.powercasting;
+ techProgression.maxClassPowerLevel =
+ SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped(techProgression.levels - 1, 0, 20)];
+ }
+
+ // Look up the number of slots per level from the powerLimit table
+ let forcePowerLimit = Array.from(SW5E.powerLimit["none"]);
+ for (let i = 0; i < forceProgression.maxClassPowerLevel; i++) {
+ forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i];
+ }
+
+ for (let [n, lvl] of Object.entries(powers)) {
+ let i = parseInt(n.slice(-1));
+ if (Number.isNaN(i)) continue;
+ if (Number.isNumeric(lvl.foverride)) lvl.fmax = Math.max(parseInt(lvl.foverride), 0);
+ else lvl.fmax = forcePowerLimit[i - 1] || 0;
+ if (isNPC) {
+ lvl.fvalue = lvl.fmax;
+ } else {
+ lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax), lvl.fmax);
+ }
+ }
+
+ let techPowerLimit = Array.from(SW5E.powerLimit["none"]);
+ for (let i = 0; i < techProgression.maxClassPowerLevel; i++) {
+ techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i];
+ }
+
+ for (let [n, lvl] of Object.entries(powers)) {
+ let i = parseInt(n.slice(-1));
+ if (Number.isNaN(i)) continue;
+ if (Number.isNumeric(lvl.toverride)) lvl.tmax = Math.max(parseInt(lvl.toverride), 0);
+ else lvl.tmax = techPowerLimit[i - 1] || 0;
+ if (isNPC) {
+ lvl.tvalue = lvl.tmax;
+ } else {
+ lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax), lvl.tmax);
+ }
+ }
+
+ // Set Force and tech power for PC Actors
+ if (!isNPC && forceProgression.levels) {
+ actorData.data.attributes.force.known.max = forceProgression.powersKnown;
+ actorData.data.attributes.force.points.max = forceProgression.points; // + Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod);
+ actorData.data.attributes.force.level = forceProgression.levels;
+ }
+ if (!isNPC && techProgression.levels) {
+ actorData.data.attributes.tech.known.max = techProgression.powersKnown;
+ actorData.data.attributes.tech.points.max = techProgression.points; // + actorData.data.abilities.int.mod;
+ actorData.data.attributes.tech.level = techProgression.levels;
+ }
+
+ // Tally Powers Known and check for migration first to avoid errors
+ let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined;
+ if (hasKnownPowers) {
+ const knownPowers = this.data.items.filter((i) => i.type === "power");
+ let knownForcePowers = 0;
+ let knownTechPowers = 0;
+ for (let knownPower of knownPowers) {
+ const d = knownPower.data;
+ switch (knownPower.data.school) {
+ case "lgt":
+ case "uni":
+ case "drk": {
+ knownForcePowers++;
+ break;
+ }
+ case "tec": {
+ knownTechPowers++;
+ break;
+ }
+ }
+ continue;
+ }
+ actorData.data.attributes.force.known.value = knownForcePowers;
+ actorData.data.attributes.tech.known.value = knownTechPowers;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare data related to the power-casting capabilities of the Actor
+ * @private
+ */
+ _computeDerivedPowercasting(actorData) {
+ if (actorData.type !== "actor") return;
+
+ // Set Force and tech power for PC Actors
+ if (!!actorData.data.attributes.force.level) {
+ actorData.data.attributes.force.points.max += Math.max(
+ actorData.data.abilities.wis.mod,
+ actorData.data.abilities.cha.mod
+ );
+ }
+ if (!!actorData.data.attributes.tech.level) {
+ actorData.data.attributes.tech.points.max += actorData.data.abilities.int.mod;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Compute the level and percentage of encumbrance for an Actor.
+ *
+ * Optionally include the weight of carried currency across all denominations by applying the standard rule
+ * from the PHB pg. 143
+ * @param {Object} actorData The data object for the Actor being rendered
+ * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level
+ * @private
+ */
+ _computeEncumbrance(actorData) {
+ // Get the total weight from items
+ const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"];
+ let weight = actorData.items.reduce((weight, i) => {
+ if (!physicalItems.includes(i.type)) return weight;
+ const q = i.data.quantity || 0;
+ const w = i.data.weight || 0;
+ return weight + q * w;
+ }, 0);
+
+ // [Optional] add Currency Weight (for non-transformed actors)
+ if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) {
+ const currency = actorData.data.currency;
+ const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0);
+ weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
+ }
+
+ // Determine the encumbrance size class
+ let mod =
+ {
+ tiny: 0.5,
+ sm: 1,
+ med: 1,
+ lg: 2,
+ huge: 4,
+ grg: 8
+ }[actorData.data.traits.size] || 1;
+ 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.toNearest(0.1), max, pct, encumbered: pct > 2 / 3};
+ }
+
+ _computeFuel(actorData) {
+ const fuel = actorData.data.attributes.fuel;
+ // Compute Fuel percentage
+ const pct = Math.clamped((fuel.value.toNearest(0.1) * 100) / fuel.cap, 0, 100);
+ return {...fuel, pct, fueled: pct > 0};
+ }
+
+ /* -------------------------------------------- */
+ /* Socket Listeners and Handlers
+ /* -------------------------------------------- */
+
+ /** @override */
+ static async create(data, options = {}) {
+ data.token = data.token || {};
+ if (data.type === "character") {
+ mergeObject(
+ data.token,
+ {
+ vision: true,
+ dimSight: 30,
+ brightSight: 0,
+ actorLink: true,
+ disposition: 1
+ },
+ {overwrite: false}
+ );
+ }
+ return super.create(data, options);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async update(data, options = {}) {
+ // 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")) {
+ let size = CONFIG.SW5E.tokenSizes[newSize];
+ if (this.isToken) this.token.update({height: size, width: size});
+ else if (!data["token.width"] && !hasProperty(data, "token.width")) {
+ data["token.height"] = size;
+ 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);
+ setProperty(data, "data.attributes.death.failure", 0);
+ }
+
+ // Perform the update
+ return super.update(data, options);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async createEmbeddedEntity(embeddedName, itemData, options = {}) {
+ // Pre-creation steps for owned items
+ if (embeddedName === "OwnedItem") this._preCreateOwnedItem(itemData, options);
+
+ // Standard embedded entity creation
+ return super.createEmbeddedEntity(embeddedName, itemData, options);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer
+ * @param itemData
+ * @param options
+ * @private
+ */
+ _preCreateOwnedItem(itemData, options) {
+ if (this.data.type === "vehicle") return;
+ const isNPC = this.data.type === "npc";
+ let initial = {};
+ switch (itemData.type) {
+ case "weapon":
+ if (getProperty(itemData, "data.equipped") === undefined) {
+ initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
+ }
+ if (getProperty(itemData, "data.proficient") === undefined) {
+ if (isNPC) {
+ initial["data.proficient"] = true; // NPCs automatically have equipment proficiency
+ } else {
+ const weaponProf = {
+ natural: true,
+ simpleVW: "sim",
+ simpleB: "sim",
+ simpleLW: "sim",
+ martialVW: "mar",
+ martialB: "mar",
+ martialLW: "mar"
+ }[itemData.data?.weaponType]; // Player characters check proficiency
+ const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
+ const hasWeaponProf = weaponProf === true || actorWeaponProfs.includes(weaponProf);
+ initial["data.proficient"] = hasWeaponProf;
+ }
+ }
+ break;
+
+ case "equipment":
+ if (getProperty(itemData, "data.equipped") === undefined) {
+ initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
+ }
+ if (getProperty(itemData, "data.proficient") === undefined) {
+ if (isNPC) {
+ initial["data.proficient"] = true; // NPCs automatically have equipment proficiency
+ } else {
+ const armorProf = {
+ natural: true,
+ clothing: true,
+ light: "lgt",
+ medium: "med",
+ heavy: "hvy",
+ shield: "shl"
+ }[itemData.data?.armor?.type]; // Player characters check proficiency
+ const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
+ const hasEquipmentProf = armorProf === true || actorArmorProfs.includes(armorProf);
+ initial["data.proficient"] = hasEquipmentProf;
+ }
+ }
+ break;
+
+ case "power":
+ initial["data.prepared"] = true; // automatically prepare powers for everyone
+ break;
+ }
+ mergeObject(itemData, initial);
+ }
+
+ /* -------------------------------------------- */
+ /* Gameplay Mechanics */
+ /* -------------------------------------------- */
+
+ /** @override */
+ async modifyTokenAttribute(attribute, value, isDelta, isBar) {
+ if (attribute === "attributes.hp") {
+ const hp = getProperty(this.data.data, attribute);
+ const delta = isDelta ? -1 * value : hp.value + hp.temp - value;
+ return this.applyDamage(delta);
+ }
+ return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Apply a certain amount of damage or healing to the health pool for Actor
+ * @param {number} amount An amount of damage (positive) or healing (negative) to sustain
+ * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing
+ * @return {Promise} A Promise which resolves once the damage has been applied
+ */
+ async applyDamage(amount = 0, multiplier = 1) {
+ amount = Math.floor(parseInt(amount) * multiplier);
+ const hp = this.data.data.attributes.hp;
+
+ // Deduct damage from temp HP first
+ const tmp = parseInt(hp.temp) || 0;
+ const dt = amount > 0 ? Math.min(tmp, amount) : 0;
+
+ // Remaining goes to health
+ const tmpMax = parseInt(hp.tempmax) || 0;
+ const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax);
+
+ // Update the Actor
+ const updates = {
+ "data.attributes.hp.temp": tmp - dt,
+ "data.attributes.hp.value": dh
+ };
+
+ // Delegate damage application to a hook
+ // TODO replace this in the future with a better modifyTokenAttribute function in the core
+ const allowed = Hooks.call(
+ "modifyTokenAttribute",
+ {
+ attribute: "attributes.hp",
+ value: amount,
+ isDelta: false,
+ isBar: true
+ },
+ updates
+ );
+ return allowed !== false ? this.update(updates) : this;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Roll a Skill Check
+ * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
+ * @param {string} skillId The skill id (e.g. "ins")
+ * @param {Object} options Options which configure how the skill check is rolled
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollSkill(skillId, options = {}) {
+ const skl = this.data.data.skills[skillId];
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+
+ // Compose roll parts and data
+ const parts = ["@mod"];
+ const data = {mod: skl.mod + skl.prof};
+
+ // Ability test bonus
+ if (bonuses.check) {
+ data["checkBonus"] = bonuses.check;
+ parts.push("@checkBonus");
+ }
+
+ // Skill check bonus
+ if (bonuses.skill) {
+ data["skillBonus"] = bonuses.skill;
+ parts.push("@skillBonus");
+ }
+
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
+
+ // Reliable Talent applies to any skill check we have full or better proficiency in
+ const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent");
+
+ // Roll and return
+ const rollData = mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.format("SW5E.SkillPromptTitle", {
+ skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]
+ }),
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ reliableTalent: reliableTalent,
+ messageData: {"flags.sw5e.roll": {type: "skill", skillId}}
});
- await ChatMessage.create({content: game.i18n.format("SW5E.DeathSaveSuccess", {name: this.name}), speaker});
- }
-
- // Increment successes
- else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)});
+ rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
+ return d20Roll(rollData);
}
- // Save failure
- else {
- let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1);
- await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)});
- if ( failures >= 3 ) { // 3 Failures = death
- await ChatMessage.create({content: game.i18n.format("SW5E.DeathSaveFailure", {name: this.name}), speaker});
- }
+ /* -------------------------------------------- */
+
+ /**
+ * Roll a generic ability test or saving throw.
+ * Prompt the user for input on which variety of roll they want to do.
+ * @param {String}abilityId The ability id (e.g. "str")
+ * @param {Object} options Options which configure how ability tests or saving throws are rolled
+ */
+ rollAbility(abilityId, options = {}) {
+ const label = CONFIG.SW5E.abilities[abilityId];
+ new Dialog({
+ title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
+ content: `${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}
`,
+ buttons: {
+ test: {
+ label: game.i18n.localize("SW5E.ActionAbil"),
+ callback: () => this.rollAbilityTest(abilityId, options)
+ },
+ save: {
+ label: game.i18n.localize("SW5E.ActionSave"),
+ callback: () => this.rollAbilitySave(abilityId, options)
+ }
+ }
+ }).render(true);
}
- // Return the rolled result
- return roll;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Roll an Ability Test
+ * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
+ * @param {String} abilityId The ability ID (e.g. "str")
+ * @param {Object} options Options which configure how ability tests are rolled
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollAbilityTest(abilityId, options = {}) {
+ const label = CONFIG.SW5E.abilities[abilityId];
+ const abl = this.data.data.abilities[abilityId];
- /**
- * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier
- * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8".
- * If no denomination is provided, the first available HD will be used
- * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll?
- * @return {Promise} The created Roll instance, or null if no hit die was rolled
- */
- async rollHitDie(denomination, {dialog=true}={}) {
+ // Construct parts
+ const parts = ["@mod"];
+ const data = {mod: abl.mod};
- // If no denomination was provided, choose the first available
- let cls = null;
- if ( !denomination ) {
- cls = this.itemTypes.class.find(c => c.data.data.hitDiceUsed < c.data.data.levels);
- if ( !cls ) return null;
- denomination = cls.data.data.hitDice;
+ // Add feat-related proficiency bonuses
+ const feats = this.data.flags.sw5e || {};
+ if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) {
+ parts.push("@proficiency");
+ data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof);
+ } else if (feats.jackOfAllTrades) {
+ parts.push("@proficiency");
+ data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof);
+ }
+
+ // Add global actor bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if (bonuses.check) {
+ parts.push("@checkBonus");
+ data.checkBonus = bonuses.check;
+ }
+
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
+
+ // Roll and return
+ const rollData = mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
+ halflingLucky: feats.halflingLucky,
+ messageData: {"flags.sw5e.roll": {type: "ability", abilityId}}
+ });
+ rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
+ return d20Roll(rollData);
}
- // Otherwise locate a class (if any) which has an available hit die of the requested denomination
- else {
- cls = this.items.find(i => {
- const d = i.data.data;
- return (d.hitDice === denomination) && ((d.hitDiceUsed || 0) < (d.levels || 1));
- });
+ /* -------------------------------------------- */
+
+ /**
+ * Roll an Ability Saving Throw
+ * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
+ * @param {String} abilityId The ability ID (e.g. "str")
+ * @param {Object} options Options which configure how ability tests are rolled
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ 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};
+
+ // Include proficiency bonus
+ if (abl.prof > 0) {
+ parts.push("@prof");
+ data.prof = abl.prof;
+ }
+
+ // Include a global actor ability save bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if (bonuses.save) {
+ parts.push("@saveBonus");
+ data.saveBonus = bonuses.save;
+ }
+
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
+
+ // Roll and return
+ const rollData = mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}),
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ messageData: {"flags.sw5e.roll": {type: "save", abilityId}}
+ });
+ rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
+ return d20Roll(rollData);
}
- // If no class is available, display an error notification
- if ( !cls ) {
- ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination}));
- return null;
+ /* -------------------------------------------- */
+
+ /**
+ * Perform a death saving throw, rolling a d20 plus any global save bonuses
+ * @param {Object} options Additional options which modify the roll
+ * @return {Promise} A Promise which resolves to the Roll instance
+ */
+ async rollDeathSave(options = {}) {
+ // Display a warning if we are not at zero HP or if we already have reached 3
+ const death = this.data.data.attributes.death;
+ if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) {
+ ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary"));
+ return null;
+ }
+
+ // Evaluate a global saving throw bonus
+ const parts = [];
+ const data = {};
+ const speaker = options.speaker || ChatMessage.getSpeaker({actor: this});
+
+ // Include a global actor ability save bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if (bonuses.save) {
+ parts.push("@saveBonus");
+ data.saveBonus = bonuses.save;
+ }
+
+ // Evaluate the roll
+ const rollData = mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.localize("SW5E.DeathSavingThrow"),
+ speaker: speaker,
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ targetValue: 10,
+ messageData: {"flags.sw5e.roll": {type: "death"}}
+ });
+ rollData.speaker = speaker;
+ const roll = await d20Roll(rollData);
+ if (!roll) return null;
+
+ // Take action depending on the result
+ const success = roll.total >= 10;
+ const d20 = roll.dice[0].total;
+
+ // Save success
+ if (success) {
+ let successes = (death.success || 0) + 1;
+
+ // Critical Success = revive with 1hp
+ if (d20 === 20) {
+ await this.update({
+ "data.attributes.death.success": 0,
+ "data.attributes.death.failure": 0,
+ "data.attributes.hp.value": 1
+ });
+ await ChatMessage.create({
+ content: game.i18n.format("SW5E.DeathSaveCriticalSuccess", {name: this.name}),
+ speaker
+ });
+ }
+
+ // 3 Successes = survive and reset checks
+ else if (successes === 3) {
+ await this.update({
+ "data.attributes.death.success": 0,
+ "data.attributes.death.failure": 0
+ });
+ await ChatMessage.create({
+ content: game.i18n.format("SW5E.DeathSaveSuccess", {name: this.name}),
+ speaker
+ });
+ }
+
+ // Increment successes
+ else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)});
+ }
+
+ // Save failure
+ else {
+ let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1);
+ await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)});
+ if (failures >= 3) {
+ // 3 Failures = death
+ await ChatMessage.create({
+ content: game.i18n.format("SW5E.DeathSaveFailure", {name: this.name}),
+ speaker
+ });
+ }
+ }
+
+ // Return the rolled result
+ return roll;
}
- // Prepare roll data
- const parts = [`1${denomination}`, "@abilities.con.mod"];
- const title = game.i18n.localize("SW5E.HitDiceRoll");
- const rollData = duplicate(this.data.data);
+ /* -------------------------------------------- */
- // Call the roll helper utility
- const roll = await damageRoll({
- event: new Event("hitDie"),
- parts: parts,
- data: rollData,
- title: title,
- speaker: ChatMessage.getSpeaker({actor: this}),
- allowcritical: false,
- fastForward: !dialog,
- dialogOptions: {width: 350},
- messageData: {"flags.sw5e.roll": {type: "hitDie"}}
- });
- if ( !roll ) return null;
+ /**
+ * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier
+ * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8".
+ * If no denomination is provided, the first available HD will be used
+ * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll?
+ * @return {Promise} The created Roll instance, or null if no hit die was rolled
+ */
+ async rollHitDie(denomination, {dialog = true} = {}) {
+ // If no denomination was provided, choose the first available
+ let cls = null;
+ if (!denomination) {
+ cls = this.itemTypes.class.find((c) => c.data.data.hitDiceUsed < c.data.data.levels);
+ if (!cls) return null;
+ denomination = cls.data.data.hitDice;
+ }
- // Adjust actor data
- await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
- const hp = this.data.data.attributes.hp;
- const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
- await this.update({"data.attributes.hp.value": hp.value + dhp});
- return roll;
- }
+ // Otherwise locate a class (if any) which has an available hit die of the requested denomination
+ else {
+ cls = this.items.find((i) => {
+ const d = i.data.data;
+ return d.hitDice === denomination && (d.hitDiceUsed || 0) < (d.levels || 1);
+ });
+ }
- /* -------------------------------------------- */
+ // If no class is available, display an error notification
+ if (!cls) {
+ ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination}));
+ return null;
+ }
- /**
- * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier
- * @param {string} [denomination] The hit denomination of hull die to roll. Example "d8".
- * If no denomination is provided, the first available HD will be used
- * @param {string} [numDice] How many damage dice to roll?
- * @param {string} [keep] Which dice to keep? Example "kh1".
- * @param {boolean} [dialog] Show a dialog prompt for configuring the hull die roll?
- * @return {Promise} The created Roll instance, or null if no hull die was rolled
- */
- async rollHullDie(denomination, numDice="1", keep="",{dialog=true}={}) {
+ // Prepare roll data
+ const parts = [`1${denomination}`, "@abilities.con.mod"];
+ const title = game.i18n.localize("SW5E.HitDiceRoll");
+ const rollData = duplicate(this.data.data);
- // If no denomination was provided, choose the first available
- let sship = null;
- if ( !denomination ) {
- sship = this.itemTypes.class.find(s => s.data.data.hullDiceUsed < (s.data.data.tier + s.data.data.hullDiceStart));
- if ( !sship ) return null;
- denomination = sship.data.data.hullDice;
+ // Call the roll helper utility
+ const roll = await damageRoll({
+ event: new Event("hitDie"),
+ parts: parts,
+ data: rollData,
+ title: title,
+ speaker: ChatMessage.getSpeaker({actor: this}),
+ allowcritical: false,
+ fastForward: !dialog,
+ dialogOptions: {width: 350},
+ messageData: {"flags.sw5e.roll": {type: "hitDie"}}
+ });
+ if (!roll) return null;
+
+ // Adjust actor data
+ await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
+ const hp = this.data.data.attributes.hp;
+ const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
+ await this.update({"data.attributes.hp.value": hp.value + dhp});
+ return roll;
}
- // Otherwise locate a starship (if any) which has an available hit die of the requested denomination
- else {
- sship = this.items.find(i => {
- const d = i.data.data;
- return (d.hullDice === denomination) && ((d.hitDiceUsed || 0) < ((d.tier || 0) + d.hullDiceStart));
- });
+ /* -------------------------------------------- */
+
+ /**
+ * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier
+ * @param {string} [denomination] The hit denomination of hull die to roll. Example "d8".
+ * If no denomination is provided, the first available HD will be used
+ * @param {string} [numDice] How many damage dice to roll?
+ * @param {string} [keep] Which dice to keep? Example "kh1".
+ * @param {boolean} [dialog] Show a dialog prompt for configuring the hull die roll?
+ * @return {Promise} The created Roll instance, or null if no hull die was rolled
+ */
+ async rollHullDie(denomination, numDice = "1", keep = "", {dialog = true} = {}) {
+ // If no denomination was provided, choose the first available
+ let sship = null;
+ if (!denomination) {
+ sship = this.itemTypes.class.find(
+ (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart
+ );
+ if (!sship) return null;
+ denomination = sship.data.data.hullDice;
+ }
+
+ // Otherwise locate a starship (if any) which has an available hit die of the requested denomination
+ else {
+ sship = this.items.find((i) => {
+ const d = i.data.data;
+ return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart;
+ });
+ }
+
+ // If no class is available, display an error notification
+ if (!sship) {
+ ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination}));
+ return null;
+ }
+
+ // Prepare roll data
+ const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"];
+ const title = game.i18n.localize("SW5E.HullDiceRoll");
+ const rollData = duplicate(this.data.data);
+
+ // Call the roll helper utility
+ const roll = await damageRoll({
+ event: new Event("hitDie"),
+ parts: parts,
+ data: rollData,
+ title: title,
+ speaker: ChatMessage.getSpeaker({actor: this}),
+ allowcritical: false,
+ fastForward: !dialog,
+ dialogOptions: {width: 350},
+ messageData: {"flags.sw5e.roll": {type: "hullDie"}}
+ });
+ if (!roll) return null;
+
+ // Adjust actor data
+ await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1});
+ const hp = this.data.data.attributes.hp;
+ const dhp = Math.min(hp.max - hp.value, roll.total);
+ await this.update({"data.attributes.hp.value": hp.value + dhp});
+ return roll;
}
- // If no class is available, display an error notification
- if ( !sship ) {
- ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination}));
- return null;
- }
+ /* -------------------------------------------- */
- // Prepare roll data
- const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"];
- const title = game.i18n.localize("SW5E.HullDiceRoll");
- const rollData = duplicate(this.data.data);
-
- // Call the roll helper utility
- const roll = await damageRoll({
- event: new Event("hitDie"),
- parts: parts,
- data: rollData,
- title: title,
- speaker: ChatMessage.getSpeaker({actor: this}),
- allowcritical: false,
- fastForward: !dialog,
- dialogOptions: {width: 350},
- messageData: {"flags.sw5e.roll": {type: "hullDie"}}
- });
- if ( !roll ) return null;
-
- // Adjust actor data
- await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1});
- const hp = this.data.data.attributes.hp;
- const dhp = Math.min(hp.max - hp.value, roll.total);
- await this.update({"data.attributes.hp.value": hp.value + dhp});
- return roll;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier
- * @return {Promise} The created Roll instance, or null if no hull die was rolled
- */
+ /**
+ * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier
+ * @return {Promise} The created Roll instance, or null if no hull die was rolled
+ */
async rollHullDieCheck() {
+ // If no denomination was provided, choose the first available
+ let sship = null;
+ if (!denomination) {
+ sship = this.itemTypes.class.find(
+ (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart
+ );
+ if (!sship) return null;
+ denomination = sship.data.data.hullDice;
+ }
- // If no denomination was provided, choose the first available
- let sship = null;
- if ( !denomination ) {
- sship = this.itemTypes.class.find(s => s.data.data.hullDiceUsed < (s.data.data.tier + s.data.data.hullDiceStart));
- if ( !sship ) return null;
- denomination = sship.data.data.hullDice;
- }
-
- // Otherwise locate a starship (if any) which has an available hit die of the requested denomination
- else {
- sship = this.items.find(i => {
- const d = i.data.data;
- return (d.hullDice === denomination) && ((d.hitDiceUsed || 0) < ((d.tier || 0) + d.hullDiceStart));
+ // Otherwise locate a starship (if any) which has an available hit die of the requested denomination
+ else {
+ sship = this.items.find((i) => {
+ const d = i.data.data;
+ return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart;
+ });
+ }
+
+ // If no class is available, display an error notification
+ if (!sship) {
+ ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination}));
+ return null;
+ }
+
+ // Prepare roll data
+ const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"];
+ const title = game.i18n.localize("SW5E.HullDiceRoll");
+ const rollData = duplicate(this.data.data);
+
+ // Call the roll helper utility
+ const roll = await damageRoll({
+ event: new Event("hitDie"),
+ parts: parts,
+ data: rollData,
+ title: title,
+ speaker: ChatMessage.getSpeaker({actor: this}),
+ allowcritical: false,
+ fastForward: !dialog,
+ dialogOptions: {width: 350},
+ messageData: {"flags.sw5e.roll": {type: "hullDie"}}
});
- }
-
- // If no class is available, display an error notification
- if ( !sship ) {
- ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination}));
- return null;
- }
-
- // Prepare roll data
- const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"];
- const title = game.i18n.localize("SW5E.HullDiceRoll");
- const rollData = duplicate(this.data.data);
-
- // Call the roll helper utility
- const roll = await damageRoll({
- event: new Event("hitDie"),
- parts: parts,
- data: rollData,
- title: title,
- speaker: ChatMessage.getSpeaker({actor: this}),
- allowcritical: false,
- fastForward: !dialog,
- dialogOptions: {width: 350},
- messageData: {"flags.sw5e.roll": {type: "hullDie"}}
- });
- if ( !roll ) return null;
-
- // Adjust actor data
- await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1});
- const hp = this.data.data.attributes.hp;
- const dhp = Math.min(hp.max - hp.value, roll.total);
- await this.update({"data.attributes.hp.value": hp.value + dhp});
- return roll;
+ if (!roll) return null;
+
+ // Adjust actor data
+ await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1});
+ const hp = this.data.data.attributes.hp;
+ const dhp = Math.min(hp.max - hp.value, roll.total);
+ await this.update({"data.attributes.hp.value": hp.value + dhp});
+ return roll;
}
-
+
/* -------------------------------------------- */
- /**
- * Roll a shield die of the appropriate type, gaining shield points equal to the die roll
- * multiplied by the shield regeneration coefficient
- * @param {string} [denomination] The denomination of shield die to roll. Example "d8".
- * If no denomination is provided, the first available SD will be used
- * @param {boolean} [natural] Natural ship shield regeneration (true) or user action (false)?
- * @param {string} [numDice] How many damage dice to roll?
- * @param {string} [keep] Which dice to keep? Example "kh1".
- * @param {boolean} [dialog] Show a dialog prompt for configuring the shield die roll?
- * @return {Promise} The created Roll instance, or null if no shield die was rolled
- */
- async rollShieldDie(denomination, natural=false, numDice="1", keep="", {dialog=true}={}) {
-
- // If no denomination was provided, choose the first available
- let sship = null;
- if ( !denomination ) {
- sship = this.itemTypes.class.find(s => s.data.data.shldDiceUsed < (s.data.data.tier + s.data.data.shldDiceStart));
- if ( !sship ) return null;
- denomination = sship.data.data.shldDice;
- }
-
- // Otherwise locate a starship (if any) which has an available hit die of the requested denomination
- else {
- sship = this.items.find(i => {
- const d = i.data.data;
- return (d.shldDice === denomination) && ((d.shldDiceUsed || 0) < ((d.tier || 0) + d.shldDiceStart));
- });
- }
-
- // If no starship is available, display an error notification
- if ( !sship ) {
- ui.notifications.error(game.i18n.format("SW5E.ShldDiceWarn", {name: this.name, formula: denomination}));
- return null;
- }
-
- // if natural regeneration roll max
- if (natural) {
- numdice = denomination.substring(1);
- denomination = "";
- keep = "";
- }
-
- // Prepare roll data
- const parts = [`${numDice}${denomination}${keep} * @attributes.regenRate`];
- const title = game.i18n.localize("SW5E.ShieldDiceRoll");
- const rollData = duplicate(this.data.data);
-
- // Call the roll helper utility
- roll = await damageRoll({
- event: new Event("shldDie"),
- parts: parts,
- data: rollData,
- title: title,
- speaker: ChatMessage.getSpeaker({actor: this}),
- allowcritical: false,
- fastForward: !dialog,
- dialogOptions: {width: 350},
- messageData: {"flags.sw5e.roll": {type: "shldDie"}}
- });
- if ( !roll ) return null;
-
- // Adjust actor data
- await sship.update({"data.shldDiceUsed": sship.data.data.shldDiceUsed + 1});
- const hp = this.data.data.attributes.hp;
- const dhp = Math.min(hp.tempmax - hp.temp, roll.total);
- await this.update({"data.attributes.hp.temp": hp.temp + dhp});
- return roll;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Cause this Actor to take a Short Rest and regain all Tech Points
- * During a Short Rest resources and limited item uses may be recovered
- * @param {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the Short Rest
- * @param {boolean} chat Summarize the results of the rest workflow as a chat message
- * @param {boolean} autoHD Automatically spend Hit Dice if you are missing 3 or more hit points
- * @param {boolean} autoHDThreshold A number of missing hit points which would trigger an automatic HD roll
- * @return {Promise} A Promise which resolves once the short rest workflow has completed
- */
- async shortRest({dialog=true, chat=true, autoHD=false, autoHDThreshold=3}={}) {
-
- // Take note of the initial hit points and number of hit dice the Actor has
- const hp = this.data.data.attributes.hp;
- const hd0 = this.data.data.attributes.hd;
- const hp0 = hp.value;
- let newDay = false;
-
- // Display a Dialog for rolling hit dice
- if ( dialog ) {
- try {
- newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0});
- } catch(err) {
- return;
- }
- }
-
- // Automatically spend hit dice
- else if ( autoHD ) {
- while ( (hp.value + autoHDThreshold) <= hp.max ) {
- const r = await this.rollHitDie(undefined, {dialog: false});
- if ( r === null ) break;
- }
- }
-
- // Note the change in HP and HD and TP which occurred
- const dhd = this.data.data.attributes.hd - hd0;
- const dhp = this.data.data.attributes.hp.value - hp0;
- const dtp = this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value;
-
- // Automatically Retore Tech Points
- this.update({"data.attributes.tech.points.value": this.data.data.attributes.tech.points.max});
-
- // Recover character resources
- const updateData = {};
- for ( let [k, r] of Object.entries(this.data.data.resources) ) {
- if ( r.max && r.sr ) {
- updateData[`data.resources.${k}.value`] = r.max;
- }
- }
-
- // Recover item uses
- const recovery = newDay ? ["sr", "day"] : ["sr"];
- const items = this.items.filter(item => item.data.data.uses && recovery.includes(item.data.data.uses.per));
- const updateItems = items.map(item => {
- return {
- _id: item._id,
- "data.uses.value": item.data.data.uses.max
- };
- });
- await this.updateEmbeddedEntity("OwnedItem", updateItems);
-
- // Display a Chat Message summarizing the rest effects
- 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)){
- if (dtp !== 0){
- srMessage = "SW5E.ShortRestResultWithTech";
- }else{
- srMessage = "SW5E.ShortRestResult";
+ /**
+ * Roll a shield die of the appropriate type, gaining shield points equal to the die roll
+ * multiplied by the shield regeneration coefficient
+ * @param {string} [denomination] The denomination of shield die to roll. Example "d8".
+ * If no denomination is provided, the first available SD will be used
+ * @param {boolean} [natural] Natural ship shield regeneration (true) or user action (false)?
+ * @param {string} [numDice] How many damage dice to roll?
+ * @param {string} [keep] Which dice to keep? Example "kh1".
+ * @param {boolean} [dialog] Show a dialog prompt for configuring the shield die roll?
+ * @return {Promise} The created Roll instance, or null if no shield die was rolled
+ */
+ async rollShieldDie(denomination, natural = false, numDice = "1", keep = "", {dialog = true} = {}) {
+ // If no denomination was provided, choose the first available
+ let sship = null;
+ if (!denomination) {
+ sship = this.itemTypes.class.find(
+ (s) => s.data.data.shldDiceUsed < s.data.data.tier + s.data.data.shldDiceStart
+ );
+ if (!sship) return null;
+ denomination = sship.data.data.shldDice;
}
- }else{
- if (dtp !== 0){
- srMessage = "SW5E.ShortRestResultOnlyTech";
+
+ // Otherwise locate a starship (if any) which has an available hit die of the requested denomination
+ else {
+ sship = this.items.find((i) => {
+ const d = i.data.data;
+ return d.shldDice === denomination && (d.shldDiceUsed || 0) < (d.tier || 0) + d.shldDiceStart;
+ });
}
- }
- // Create a chat message
- ChatMessage.create({
- user: game.user._id,
- speaker: {actor: this, alias: this.name},
- flavor: restFlavor,
- content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp, tech: dtp})
- });
+ // If no starship is available, display an error notification
+ if (!sship) {
+ ui.notifications.error(game.i18n.format("SW5E.ShldDiceWarn", {name: this.name, formula: denomination}));
+ return null;
+ }
+
+ // if natural regeneration roll max
+ if (natural) {
+ numdice = denomination.substring(1);
+ denomination = "";
+ keep = "";
+ }
+
+ // Prepare roll data
+ const parts = [`${numDice}${denomination}${keep} * @attributes.regenRate`];
+ const title = game.i18n.localize("SW5E.ShieldDiceRoll");
+ const rollData = duplicate(this.data.data);
+
+ // Call the roll helper utility
+ roll = await damageRoll({
+ event: new Event("shldDie"),
+ parts: parts,
+ data: rollData,
+ title: title,
+ speaker: ChatMessage.getSpeaker({actor: this}),
+ allowcritical: false,
+ fastForward: !dialog,
+ dialogOptions: {width: 350},
+ messageData: {"flags.sw5e.roll": {type: "shldDie"}}
+ });
+ if (!roll) return null;
+
+ // Adjust actor data
+ await sship.update({"data.shldDiceUsed": sship.data.data.shldDiceUsed + 1});
+ const hp = this.data.data.attributes.hp;
+ const dhp = Math.min(hp.tempmax - hp.temp, roll.total);
+ await this.update({"data.attributes.hp.temp": hp.temp + dhp});
+ return roll;
}
- // Return data summarizing the rest effects
- return {
- dhd: dhd,
- dhp: dhp,
- dtp: dtp,
- updateData: updateData,
- updateItems: updateItems,
- newDay: newDay
- }
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Cause this Actor to take a Short Rest and regain all Tech Points
+ * During a Short Rest resources and limited item uses may be recovered
+ * @param {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the Short Rest
+ * @param {boolean} chat Summarize the results of the rest workflow as a chat message
+ * @param {boolean} autoHD Automatically spend Hit Dice if you are missing 3 or more hit points
+ * @param {boolean} autoHDThreshold A number of missing hit points which would trigger an automatic HD roll
+ * @return {Promise} A Promise which resolves once the short rest workflow has completed
+ */
+ async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) {
+ // Take note of the initial hit points and number of hit dice the Actor has
+ const hp = this.data.data.attributes.hp;
+ const hd0 = this.data.data.attributes.hd;
+ const hp0 = hp.value;
+ let newDay = false;
- /**
- * Take a long rest, recovering HP, HD, resources, Force and Power points 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, newDay=true}={}) {
- const data = this.data.data;
+ // Display a Dialog for rolling hit dice
+ if (dialog) {
+ try {
+ newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0});
+ } catch (err) {
+ return;
+ }
+ }
- // Maybe present a confirmation dialog
- if ( dialog ) {
- try {
- newDay = await LongRestDialog.longRestDialog({actor: this});
- } catch(err) {
- return;
- }
+ // Automatically spend hit dice
+ else if (autoHD) {
+ while (hp.value + autoHDThreshold <= hp.max) {
+ const r = await this.rollHitDie(undefined, {dialog: false});
+ if (r === null) break;
+ }
+ }
+
+ // Note the change in HP and HD and TP which occurred
+ const dhd = this.data.data.attributes.hd - hd0;
+ const dhp = this.data.data.attributes.hp.value - hp0;
+ const dtp = this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value;
+
+ // Automatically Retore Tech Points
+ this.update({"data.attributes.tech.points.value": this.data.data.attributes.tech.points.max});
+
+ // Recover character resources
+ const updateData = {};
+ for (let [k, r] of Object.entries(this.data.data.resources)) {
+ if (r.max && r.sr) {
+ updateData[`data.resources.${k}.value`] = r.max;
+ }
+ }
+
+ // Recover item uses
+ const recovery = newDay ? ["sr", "day"] : ["sr"];
+ const items = this.items.filter((item) => item.data.data.uses && recovery.includes(item.data.data.uses.per));
+ const updateItems = items.map((item) => {
+ return {
+ "_id": item._id,
+ "data.uses.value": item.data.data.uses.max
+ };
+ });
+ await this.updateEmbeddedEntity("OwnedItem", updateItems);
+
+ // Display a Chat Message summarizing the rest effects
+ 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) {
+ if (dtp !== 0) {
+ srMessage = "SW5E.ShortRestResultWithTech";
+ } else {
+ srMessage = "SW5E.ShortRestResult";
+ }
+ } else {
+ if (dtp !== 0) {
+ srMessage = "SW5E.ShortRestResultOnlyTech";
+ }
+ }
+
+ // Create a chat message
+ ChatMessage.create({
+ user: game.user._id,
+ speaker: {actor: this, alias: this.name},
+ flavor: restFlavor,
+ content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp, tech: dtp})
+ });
+ }
+
+ // Return data summarizing the rest effects
+ return {
+ dhd: dhd,
+ dhp: dhp,
+ dtp: dtp,
+ updateData: updateData,
+ updateItems: updateItems,
+ newDay: newDay
+ };
}
- // Recover hit, tech, and force points to full, and eliminate any existing temporary HP, TP, and FP
- const dhp = data.attributes.hp.max - data.attributes.hp.value;
- const dtp = data.attributes.tech.points.max - data.attributes.tech.points.value;
- const dfp = data.attributes.force.points.max - data.attributes.force.points.value;
- const updateData = {
- "data.attributes.hp.value": data.attributes.hp.max,
- "data.attributes.hp.temp": 0,
- "data.attributes.hp.tempmax": 0,
- "data.attributes.tech.points.value": data.attributes.tech.points.max,
- "data.attributes.tech.points.temp": 0,
- "data.attributes.tech.points.tempmax": 0,
- "data.attributes.force.points.value": data.attributes.force.points.max,
- "data.attributes.force.points.temp": 0,
- "data.attributes.force.points.tempmax": 0
- };
+ /* -------------------------------------------- */
- // Recover character resources
- for ( let [k, r] of Object.entries(data.resources) ) {
- if ( r.max && (r.sr || r.lr) ) {
- updateData[`data.resources.${k}.value`] = r.max;
- }
+ /**
+ * Take a long rest, recovering HP, HD, resources, Force and Power points 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, newDay = true} = {}) {
+ const data = this.data.data;
+
+ // Maybe present a confirmation dialog
+ if (dialog) {
+ try {
+ newDay = await LongRestDialog.longRestDialog({actor: this});
+ } catch (err) {
+ return;
+ }
+ }
+
+ // Recover hit, tech, and force points to full, and eliminate any existing temporary HP, TP, and FP
+ const dhp = data.attributes.hp.max - data.attributes.hp.value;
+ const dtp = data.attributes.tech.points.max - data.attributes.tech.points.value;
+ const dfp = data.attributes.force.points.max - data.attributes.force.points.value;
+ const updateData = {
+ "data.attributes.hp.value": data.attributes.hp.max,
+ "data.attributes.hp.temp": 0,
+ "data.attributes.hp.tempmax": 0,
+ "data.attributes.tech.points.value": data.attributes.tech.points.max,
+ "data.attributes.tech.points.temp": 0,
+ "data.attributes.tech.points.tempmax": 0,
+ "data.attributes.force.points.value": data.attributes.force.points.max,
+ "data.attributes.force.points.temp": 0,
+ "data.attributes.force.points.tempmax": 0
+ };
+
+ // Recover character resources
+ for (let [k, r] of Object.entries(data.resources)) {
+ if (r.max && (r.sr || r.lr)) {
+ updateData[`data.resources.${k}.value`] = r.max;
+ }
+ }
+
+ // Recover power slots
+ for (let [k, v] of Object.entries(data.powers)) {
+ updateData[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : v.fmax ?? 0;
+ }
+ for (let [k, v] of Object.entries(data.powers)) {
+ updateData[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : v.tmax ?? 0;
+ }
+ // Determine the number of hit dice which may be recovered
+ let recoverHD = Math.max(Math.floor(data.details.level / 2), 1);
+ let dhd = 0;
+
+ // Sort classes which can recover HD, assuming players prefer recovering larger HD first.
+ const updateItems = this.items
+ .filter((item) => item.data.type === "class")
+ .sort((a, b) => {
+ let da = parseInt(a.data.data.hitDice.slice(1)) || 0;
+ let db = parseInt(b.data.data.hitDice.slice(1)) || 0;
+ return db - da;
+ })
+ .reduce((updates, item) => {
+ const d = item.data.data;
+ if (recoverHD > 0 && d.hitDiceUsed > 0) {
+ let delta = Math.min(d.hitDiceUsed || 0, recoverHD);
+ recoverHD -= delta;
+ dhd += delta;
+ updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta});
+ }
+ return updates;
+ }, []);
+
+ // Iterate over owned items, restoring uses per day and recovering Hit Dice
+ const recovery = newDay ? ["sr", "lr", "day"] : ["sr", "lr"];
+ for (let item of this.items) {
+ const d = item.data.data;
+ if (d.uses && recovery.includes(d.uses.per)) {
+ updateItems.push({"_id": item.id, "data.uses.value": d.uses.max});
+ } else if (d.recharge && d.recharge.value) {
+ updateItems.push({"_id": item.id, "data.recharge.charged": true});
+ }
+ }
+
+ // Perform the updates
+ await this.update(updateData);
+ if (updateItems.length) 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(newDay ? "SW5E.LongRestOvernight" : "SW5E.LongRestNormal");
+ break;
+ case "gritty":
+ restFlavor = game.i18n.localize("SW5E.LongRestGritty");
+ break;
+ case "epic":
+ restFlavor = game.i18n.localize("SW5E.LongRestEpic");
+ break;
+ }
+
+ // Determine the chat message to display
+ if (chat) {
+ let lrMessage = "SW5E.LongRestResult";
+ if (dhp !== 0) lrMessage += "HP";
+ if (dfp !== 0) lrMessage += "FP";
+ if (dtp !== 0) lrMessage += "TP";
+ if (dhd !== 0) lrMessage += "HD";
+ ChatMessage.create({
+ user: game.user._id,
+ speaker: {actor: this, alias: this.name},
+ flavor: restFlavor,
+ content: game.i18n.format(lrMessage, {name: this.name, health: dhp, tech: dtp, force: dfp, dice: dhd})
+ });
+ }
+
+ // Return data summarizing the rest effects
+ return {
+ dhd: dhd,
+ dhp: dhp,
+ dtp: dtp,
+ dfp: dfp,
+ updateData: updateData,
+ updateItems: updateItems,
+ newDay: newDay
+ };
}
- // Recover power slots
- for ( let [k, v] of Object.entries(data.powers) ) {
- updateData[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : (v.fmax ?? 0);
- }
- for ( let [k, v] of Object.entries(data.powers) ) {
- updateData[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : (v.tmax ?? 0);
- }
- // Determine the number of hit dice which may be recovered
- let recoverHD = Math.max(Math.floor(data.details.level / 2), 1);
- let dhd = 0;
+ /* -------------------------------------------- */
- // Sort classes which can recover HD, assuming players prefer recovering larger HD first.
- const updateItems = this.items.filter(item => item.data.type === "class").sort((a, b) => {
- let da = parseInt(a.data.data.hitDice.slice(1)) || 0;
- let db = parseInt(b.data.data.hitDice.slice(1)) || 0;
- return db - da;
- }).reduce((updates, item) => {
- const d = item.data.data;
- if ( (recoverHD > 0) && (d.hitDiceUsed > 0) ) {
- let delta = Math.min(d.hitDiceUsed || 0, recoverHD);
- recoverHD -= delta;
- dhd += delta;
- updates.push({_id: item.id, "data.hitDiceUsed": d.hitDiceUsed - delta});
- }
- return updates;
- }, []);
+ /**
+ * Deploy an Actor into this one.
+ *
+ * @param {Actor} target The Actor to be deployed.
+ * @param {boolean} [coord] Deploy as Coordinator
+ * @param {boolean} [gunner] Deploy as Gunner
+ * @param {boolean} [mech] Deploy as Mechanic
+ * @param {boolean} [oper] Deploy as Operator
+ * @param {boolean} [pilot] Deploy as Pilot
+ * @param {boolean} [tech] Deploy as Technician
+ * @param {boolean} [crew] Deploy as Crew
+ * @param {boolean} [pass] Deploy as Passenger
+ */
+ async deployInto(
+ target,
+ {
+ coord = false,
+ gunner = false,
+ mech = false,
+ oper = false,
+ pilot = false,
+ tech = false,
+ crew = false,
+ pass = false
+ } = {}
+ ) {
+ // Get the starship Actor data and the new char data
+ const sship = duplicate(this.toJSON());
+ const ssDeploy = sship.data.attributes.deployment;
+ const char = target;
+ const charUUID = char.uuid;
+ const charName = char.data.name;
+ const charRank = char.data.data.attributes.rank;
+ let charProf = 0;
+ if (charRank.total > 0) {
+ charProf = char.data.data.attributes.prof;
+ }
- // Iterate over owned items, restoring uses per day and recovering Hit Dice
- const recovery = newDay ? ["sr", "lr", "day"] : ["sr", "lr"];
- for ( let item of this.items ) {
- const d = item.data.data;
- if ( d.uses && recovery.includes(d.uses.per) ) {
- updateItems.push({_id: item.id, "data.uses.value": d.uses.max});
- }
- else if ( d.recharge && d.recharge.value ) {
- updateItems.push({_id: item.id, "data.recharge.charged": true});
- }
+ if (coord) {
+ ssDeploy.coord.uuid = charUUID;
+ ssDeploy.coord.name = charName;
+ ssDeploy.coord.rank = charRank.coord;
+ ssDeploy.coord.prof = charProf;
+ }
+
+ if (gunner) {
+ ssDeploy.gunner.uuid = charUUID;
+ ssDeploy.gunner.name = charName;
+ ssDeploy.gunner.rank = charRank.gunner;
+ ssDeploy.gunner.prof = charProf;
+ }
+
+ if (mech) {
+ ssDeploy.mechanic.uuid = charUUID;
+ ssDeploy.mechanic.name = charName;
+ ssDeploy.mechanic.rank = charRank.mechanic;
+ ssDeploy.mechanic.prof = charProf;
+ }
+
+ if (oper) {
+ ssDeploy.operator.uuid = charUUID;
+ ssDeploy.operator.name = charName;
+ ssDeploy.operator.rank = charRank.operator;
+ ssDeploy.operator.prof = charProf;
+ }
+
+ if (pilot) {
+ ssDeploy.pilot.uuid = charUUID;
+ ssDeploy.pilot.name = charName;
+ ssDeploy.pilot.rank = charRank.pilot;
+ ssDeploy.pilot.prof = charProf;
+ }
+
+ if (tech) {
+ ssDeploy.technician.uuid = charUUID;
+ ssDeploy.technician.name = charName;
+ ssDeploy.technician.rank = charRank.technician;
+ ssDeploy.technician.prof = charProf;
+ }
+
+ if (crew) {
+ ssDeploy.crew.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf});
+ }
+
+ if (pass) {
+ ssDeploy.passenger.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf});
+ }
+ this.update({"data.attributes.deployment": ssDeploy});
}
- // Perform the updates
- await this.update(updateData);
- if ( updateItems.length ) 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(newDay ? "SW5E.LongRestOvernight" : "SW5E.LongRestNormal"); break;
- case 'gritty': restFlavor = game.i18n.localize("SW5E.LongRestGritty"); break;
- case 'epic': restFlavor = game.i18n.localize("SW5E.LongRestEpic"); break;
- }
-
- // Determine the chat message to display
- if ( chat ) {
- let lrMessage = "SW5E.LongRestResult";
- if (dhp !== 0) lrMessage += "HP";
- if (dfp !== 0) lrMessage += "FP";
- if (dtp !== 0) lrMessage += "TP";
- if (dhd !== 0) lrMessage += "HD";
- ChatMessage.create({
- user: game.user._id,
- speaker: {actor: this, alias: this.name},
- flavor: restFlavor,
- content: game.i18n.format(lrMessage, {name: this.name, health: dhp, tech: dtp, force: dfp, dice: dhd})
- });
- }
-
- // Return data summarizing the rest effects
- return {
- dhd: dhd,
- dhp: dhp,
- dtp: dtp,
- dfp: dfp,
- updateData: updateData,
- updateItems: updateItems,
- newDay: newDay
- }
- }
-
- /* -------------------------------------------- */
-
-
- /**
- * Deploy an Actor into this one.
- *
- * @param {Actor} target The Actor to be deployed.
- * @param {boolean} [coord] Deploy as Coordinator
- * @param {boolean} [gunner] Deploy as Gunner
- * @param {boolean} [mech] Deploy as Mechanic
- * @param {boolean} [oper] Deploy as Operator
- * @param {boolean} [pilot] Deploy as Pilot
- * @param {boolean} [tech] Deploy as Technician
- * @param {boolean} [crew] Deploy as Crew
- * @param {boolean} [pass] Deploy as Passenger
- */
- async deployInto(target, { coord=false, gunner=false, mech=false, oper=false,
- pilot=false, tech=false, crew=false, pass=false}={}) {
-
- // Get the starship Actor data and the new char data
- const sship = duplicate(this.toJSON());
- const ssDeploy = sship.data.attributes.deployment;
- const char = target;
- const charUUID = char.uuid;
- const charName = char.data.name;
- const charRank = char.data.data.attributes.rank;
- let charProf = 0;
- if (charRank.total > 0) {
- charProf = char.data.data.attributes.prof;
- }
-
- if (coord){
- ssDeploy.coord.uuid = charUUID;
- ssDeploy.coord.name = charName;
- ssDeploy.coord.rank = charRank.coord;
- ssDeploy.coord.prof = charProf;
- }
-
- if (gunner){
- ssDeploy.gunner.uuid = charUUID;
- ssDeploy.gunner.name = charName;
- ssDeploy.gunner.rank = charRank.gunner;
- ssDeploy.gunner.prof = charProf;
- }
-
- if (mech){
- ssDeploy.mechanic.uuid = charUUID;
- ssDeploy.mechanic.name = charName;
- ssDeploy.mechanic.rank = charRank.mechanic;
- ssDeploy.mechanic.prof = charProf;
- }
-
- if (oper){
- ssDeploy.operator.uuid = charUUID;
- ssDeploy.operator.name = charName;
- ssDeploy.operator.rank = charRank.operator;
- ssDeploy.operator.prof = charProf;
- }
-
- if (pilot){
- ssDeploy.pilot.uuid = charUUID;
- ssDeploy.pilot.name = charName;
- ssDeploy.pilot.rank = charRank.pilot;
- ssDeploy.pilot.prof = charProf;
- }
-
- if (tech){
- ssDeploy.technician.uuid = charUUID;
- ssDeploy.technician.name = charName;
- ssDeploy.technician.rank = charRank.technician;
- ssDeploy.technician.prof = charProf;
- }
-
- if (crew){
- ssDeploy.crew.push({"uuid": charUUID, "name": charName, "rank": charRank, "prof": charProf});
- }
-
- if (pass){
- ssDeploy.passenger.push({"uuid": charUUID, "name": charName, "rank": charRank, "prof": charProf});
- }
- this.update({"data.attributes.deployment": ssDeploy});
- }
-
-
- /**
- * Transform this Actor into another one.
- *
- * @param {Actor} target The target Actor.
- * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con)
- * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha)
- * @param {boolean} [keepSaves] Keep saving throw proficiencies
- * @param {boolean} [keepSkills] Keep skill proficiencies
- * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies
- * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies
- * @param {boolean} [keepClass] Keep proficiency bonus
- * @param {boolean} [keepFeats] Keep features
- * @param {boolean} [keepPowers] Keep powers
- * @param {boolean} [keepItems] Keep items
- * @param {boolean} [keepBio] Keep biography
- * @param {boolean} [keepVision] Keep vision
- * @param {boolean} [transformTokens] Transform linked tokens too
- */
- async transformInto(target, { keepPhysical=false, keepMental=false, keepSaves=false, keepSkills=false,
- mergeSaves=false, mergeSkills=false, keepClass=false, keepFeats=false, keepPowers=false,
- keepItems=false, keepBio=false, keepVision=false, transformTokens=true}={}) {
-
- // Ensure the player is allowed to polymorph
- const allowed = game.settings.get("sw5e", "allowPolymorphing");
- if ( !allowed && !game.user.isGM ) {
- return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn"));
- }
-
- // Get the original Actor data and the new source data
- const o = duplicate(this.toJSON());
- o.flags.sw5e = o.flags.sw5e || {};
- o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves};
- const source = duplicate(target.toJSON());
-
- // Prepare new data to merge from the source
- const d = {
- type: o.type, // Remain the same actor type
- name: `${o.name} (${source.name})`, // Append the new shape to your old name
- data: source.data, // Get the data model of your new form
- items: source.items, // Get the items of your new form
- effects: o.effects.concat(source.effects), // Combine active effects from both forms
- token: source.token, // New token configuration
- img: source.img, // New appearance
- permission: o.permission, // Use the original actor permissions
- folder: o.folder, // Be displayed in the same sidebar folder
- flags: o.flags // Use the original actor flags
- };
-
- // Additional adjustments
- delete d.data.resources; // Don't change your resource pools
- delete d.data.currency; // Don't lose currency
- delete d.data.bonuses; // Don't lose global bonuses
- delete d.token.actorId; // Don't reference the old actor ID
- d.token.actorLink = o.token.actorLink; // Keep your actor link
- d.token.name = d.name; // Token name same as actor name
- d.data.details.alignment = o.data.details.alignment; // Don't change alignment
- d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level
- d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration
- d.data.powers = o.data.powers; // Keep power slots
-
- // Handle wildcard
- if ( source.token.randomImg ) {
- const images = await target.getTokenImages();
- d.token.img = images[Math.floor(Math.random() * images.length)];
- }
-
- // Keep Token configurations
- const tokenConfig = ["displayName", "vision", "actorLink", "disposition", "displayBars", "bar1", "bar2"];
- if ( keepVision ) {
- tokenConfig.push(...['dimSight', 'brightSight', 'dimLight', 'brightLight', 'vision', 'sightAngle']);
- }
- for ( let c of tokenConfig ) {
- d.token[c] = o.token[c];
- }
-
- // Transfer ability scores
- const abilities = d.data.abilities;
- for ( let k of Object.keys(abilities) ) {
- const oa = o.data.abilities[k];
- const prof = abilities[k].proficient;
- if ( keepPhysical && ["str", "dex", "con"].includes(k) ) abilities[k] = oa;
- else if ( keepMental && ["int", "wis", "cha"].includes(k) ) abilities[k] = oa;
- if ( keepSaves ) abilities[k].proficient = oa.proficient;
- else if ( mergeSaves ) abilities[k].proficient = Math.max(prof, oa.proficient);
- }
-
- // Transfer skills
- if ( keepSkills ) d.data.skills = o.data.skills;
- else if ( mergeSkills ) {
- for ( let [k, s] of Object.entries(d.data.skills) ) {
- s.value = Math.max(s.value, o.data.skills[k].value);
- }
- }
-
- // Keep specific items from the original data
- d.items = d.items.concat(o.items.filter(i => {
- if ( i.type === "class" ) return keepClass;
- else if ( i.type === "feat" ) return keepFeats;
- else if ( i.type === "power" ) return keepPowers;
- else return keepItems;
- }));
-
- // Transfer classes for NPCs
- if (!keepClass && d.data.details.cr) {
- d.items.push({
- type: 'class',
- name: game.i18n.localize('SW5E.PolymorphTmpClass'),
- data: { levels: d.data.details.cr }
- });
- }
-
- // Keep biography
- if (keepBio) d.data.details.biography = o.data.details.biography;
-
- // Keep senses
- if (keepVision) d.data.traits.senses = o.data.traits.senses;
-
- // Set new data flags
- if ( !this.isPolymorphed || !d.flags.sw5e.originalActor ) d.flags.sw5e.originalActor = this.id;
- d.flags.sw5e.isPolymorphed = true;
-
- // Update unlinked Tokens in place since they can simply be re-dropped from the base actor
- if (this.isToken) {
- const tokenData = d.token;
- tokenData.actorData = d;
- delete tokenData.actorData.token;
- return this.token.update(tokenData);
- }
-
- // Update regular Actors by creating a new Actor with the Polymorphed data
- await this.sheet.close();
- Hooks.callAll('sw5e.transformActor', this, target, d, {
- keepPhysical, keepMental, keepSaves, keepSkills, mergeSaves, mergeSkills,
- keepClass, keepFeats, keepPowers, keepItems, keepBio, keepVision, transformTokens
- });
- const newActor = await this.constructor.create(d, {renderSheet: true});
-
- // Update placed Token instances
- if ( !transformTokens ) return;
- const tokens = this.getActiveTokens(true);
- const updates = tokens.map(t => {
- const newTokenData = duplicate(d.token);
- if ( !t.data.actorLink ) newTokenData.actorData = newActor.data;
- newTokenData._id = t.data._id;
- newTokenData.actorId = newActor.id;
- return newTokenData;
- });
- return canvas.scene?.updateEmbeddedEntity("Token", updates);
- }
-
- /* -------------------------------------------- */
-
- /**
- * If this actor was transformed with transformTokens enabled, then its
- * active tokens need to be returned to their original state. If not, then
- * we can safely just delete this actor.
- */
- async revertOriginalForm() {
- if ( !this.isPolymorphed ) return;
- if ( !this.owner ) {
- return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn"));
- }
-
- // If we are reverting an unlinked token, simply replace it with the base actor prototype
- if ( this.isToken ) {
- const baseActor = game.actors.get(this.token.data.actorId);
- const prototypeTokenData = duplicate(baseActor.token);
- prototypeTokenData.actorData = null;
- return this.token.update(prototypeTokenData);
- }
-
- // Obtain a reference to the original actor
- const original = game.actors.get(this.getFlag('sw5e', 'originalActor'));
- if ( !original ) return;
-
- // Get the Tokens which represent this actor
- 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;
- if ( game.user.isGM ) await this.delete();
- original.sheet.render(isRendered);
- return original;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Add additional system-specific sidebar directory context menu options for SW5e Actor entities
- * @param {jQuery} html The sidebar HTML
- * @param {Array} entryOptions The default array of context menu options
- */
- static addDirectoryContextOptions(html, entryOptions) {
- entryOptions.push({
- name: 'SW5E.PolymorphRestoreTransformation',
- icon: ' ',
- callback: li => {
- const actor = game.actors.get(li.data('entityId'));
- return actor.revertOriginalForm();
- },
- condition: li => {
+ /**
+ * Transform this Actor into another one.
+ *
+ * @param {Actor} target The target Actor.
+ * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con)
+ * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha)
+ * @param {boolean} [keepSaves] Keep saving throw proficiencies
+ * @param {boolean} [keepSkills] Keep skill proficiencies
+ * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies
+ * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies
+ * @param {boolean} [keepClass] Keep proficiency bonus
+ * @param {boolean} [keepFeats] Keep features
+ * @param {boolean} [keepPowers] Keep powers
+ * @param {boolean} [keepItems] Keep items
+ * @param {boolean} [keepBio] Keep biography
+ * @param {boolean} [keepVision] Keep vision
+ * @param {boolean} [transformTokens] Transform linked tokens too
+ */
+ async transformInto(
+ target,
+ {
+ keepPhysical = false,
+ keepMental = false,
+ keepSaves = false,
+ keepSkills = false,
+ mergeSaves = false,
+ mergeSkills = false,
+ keepClass = false,
+ keepFeats = false,
+ keepPowers = false,
+ keepItems = false,
+ keepBio = false,
+ keepVision = false,
+ transformTokens = true
+ } = {}
+ ) {
+ // Ensure the player is allowed to polymorph
const allowed = game.settings.get("sw5e", "allowPolymorphing");
- if ( !allowed && !game.user.isGM ) return false;
- const actor = game.actors.get(li.data('entityId'));
- return actor && actor.isPolymorphed;
- }
- });
- }
+ if (!allowed && !game.user.isGM) {
+ return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn"));
+ }
- /* -------------------------------------------- */
- /* DEPRECATED METHODS */
- /* -------------------------------------------- */
+ // Get the original Actor data and the new source data
+ const o = duplicate(this.toJSON());
+ o.flags.sw5e = o.flags.sw5e || {};
+ o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves};
+ const source = duplicate(target.toJSON());
- /**
- * @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;
- }
+ // Prepare new data to merge from the source
+ const d = {
+ type: o.type, // Remain the same actor type
+ name: `${o.name} (${source.name})`, // Append the new shape to your old name
+ data: source.data, // Get the data model of your new form
+ items: source.items, // Get the items of your new form
+ effects: o.effects.concat(source.effects), // Combine active effects from both forms
+ token: source.token, // New token configuration
+ img: source.img, // New appearance
+ permission: o.permission, // Use the original actor permissions
+ folder: o.folder, // Be displayed in the same sidebar folder
+ flags: o.flags // Use the original actor flags
+ };
- /* -------------------------------------------- */
+ // Additional adjustments
+ delete d.data.resources; // Don't change your resource pools
+ delete d.data.currency; // Don't lose currency
+ delete d.data.bonuses; // Don't lose global bonuses
+ delete d.token.actorId; // Don't reference the old actor ID
+ d.token.actorLink = o.token.actorLink; // Keep your actor link
+ d.token.name = d.name; // Token name same as actor name
+ d.data.details.alignment = o.data.details.alignment; // Don't change alignment
+ d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level
+ d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration
+ d.data.powers = o.data.powers; // Keep power slots
- /**
- * Cast a Power, consuming a power slot of a certain level
- * @param {Item5e} item The power being cast by the actor
- * @param {Event} event The originating user interaction which triggered the cast
- * @deprecated since sw5e 1.2.0
- */
- async usePower(item, {configureDialog=true}={}) {
- console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
- if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
- return item.roll();
- }
-}
\ No newline at end of file
+ // Handle wildcard
+ if (source.token.randomImg) {
+ const images = await target.getTokenImages();
+ d.token.img = images[Math.floor(Math.random() * images.length)];
+ }
+
+ // Keep Token configurations
+ const tokenConfig = ["displayName", "vision", "actorLink", "disposition", "displayBars", "bar1", "bar2"];
+ if (keepVision) {
+ tokenConfig.push(...["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]);
+ }
+ for (let c of tokenConfig) {
+ d.token[c] = o.token[c];
+ }
+
+ // Transfer ability scores
+ const abilities = d.data.abilities;
+ for (let k of Object.keys(abilities)) {
+ const oa = o.data.abilities[k];
+ const prof = abilities[k].proficient;
+ if (keepPhysical && ["str", "dex", "con"].includes(k)) abilities[k] = oa;
+ else if (keepMental && ["int", "wis", "cha"].includes(k)) abilities[k] = oa;
+ if (keepSaves) abilities[k].proficient = oa.proficient;
+ else if (mergeSaves) abilities[k].proficient = Math.max(prof, oa.proficient);
+ }
+
+ // Transfer skills
+ if (keepSkills) d.data.skills = o.data.skills;
+ else if (mergeSkills) {
+ for (let [k, s] of Object.entries(d.data.skills)) {
+ s.value = Math.max(s.value, o.data.skills[k].value);
+ }
+ }
+
+ // Keep specific items from the original data
+ d.items = d.items.concat(
+ o.items.filter((i) => {
+ if (i.type === "class") return keepClass;
+ else if (i.type === "feat") return keepFeats;
+ else if (i.type === "power") return keepPowers;
+ else return keepItems;
+ })
+ );
+
+ // Transfer classes for NPCs
+ if (!keepClass && d.data.details.cr) {
+ d.items.push({
+ type: "class",
+ name: game.i18n.localize("SW5E.PolymorphTmpClass"),
+ data: {levels: d.data.details.cr}
+ });
+ }
+
+ // Keep biography
+ if (keepBio) d.data.details.biography = o.data.details.biography;
+
+ // Keep senses
+ if (keepVision) d.data.traits.senses = o.data.traits.senses;
+
+ // Set new data flags
+ if (!this.isPolymorphed || !d.flags.sw5e.originalActor) d.flags.sw5e.originalActor = this.id;
+ d.flags.sw5e.isPolymorphed = true;
+
+ // Update unlinked Tokens in place since they can simply be re-dropped from the base actor
+ if (this.isToken) {
+ const tokenData = d.token;
+ tokenData.actorData = d;
+ delete tokenData.actorData.token;
+ return this.token.update(tokenData);
+ }
+
+ // Update regular Actors by creating a new Actor with the Polymorphed data
+ await this.sheet.close();
+ Hooks.callAll("sw5e.transformActor", this, target, d, {
+ keepPhysical,
+ keepMental,
+ keepSaves,
+ keepSkills,
+ mergeSaves,
+ mergeSkills,
+ keepClass,
+ keepFeats,
+ keepPowers,
+ keepItems,
+ keepBio,
+ keepVision,
+ transformTokens
+ });
+ const newActor = await this.constructor.create(d, {renderSheet: true});
+
+ // Update placed Token instances
+ if (!transformTokens) return;
+ const tokens = this.getActiveTokens(true);
+ const updates = tokens.map((t) => {
+ const newTokenData = duplicate(d.token);
+ if (!t.data.actorLink) newTokenData.actorData = newActor.data;
+ newTokenData._id = t.data._id;
+ newTokenData.actorId = newActor.id;
+ return newTokenData;
+ });
+ return canvas.scene?.updateEmbeddedEntity("Token", updates);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * If this actor was transformed with transformTokens enabled, then its
+ * active tokens need to be returned to their original state. If not, then
+ * we can safely just delete this actor.
+ */
+ async revertOriginalForm() {
+ if (!this.isPolymorphed) return;
+ if (!this.owner) {
+ return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn"));
+ }
+
+ // If we are reverting an unlinked token, simply replace it with the base actor prototype
+ if (this.isToken) {
+ const baseActor = game.actors.get(this.token.data.actorId);
+ const prototypeTokenData = duplicate(baseActor.token);
+ prototypeTokenData.actorData = null;
+ return this.token.update(prototypeTokenData);
+ }
+
+ // Obtain a reference to the original actor
+ const original = game.actors.get(this.getFlag("sw5e", "originalActor"));
+ if (!original) return;
+
+ // Get the Tokens which represent this actor
+ 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;
+ if (game.user.isGM) await this.delete();
+ original.sheet.render(isRendered);
+ return original;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Add additional system-specific sidebar directory context menu options for SW5e Actor entities
+ * @param {jQuery} html The sidebar HTML
+ * @param {Array} entryOptions The default array of context menu options
+ */
+ static addDirectoryContextOptions(html, entryOptions) {
+ entryOptions.push({
+ name: "SW5E.PolymorphRestoreTransformation",
+ icon: ' ',
+ callback: (li) => {
+ const actor = game.actors.get(li.data("entityId"));
+ return actor.revertOriginalForm();
+ },
+ condition: (li) => {
+ const allowed = game.settings.get("sw5e", "allowPolymorphing");
+ if (!allowed && !game.user.isGM) return false;
+ const actor = game.actors.get(li.data("entityId"));
+ return actor && actor.isPolymorphed;
+ }
+ });
+ }
+
+ /* -------------------------------------------- */
+ /* 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;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Cast a Power, consuming a power slot of a certain level
+ * @param {Item5e} item The power being cast by the actor
+ * @param {Event} event The originating user interaction which triggered the cast
+ * @deprecated since sw5e 1.2.0
+ */
+ async usePower(item, {configureDialog = true} = {}) {
+ console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
+ if (item.data.type !== "power") throw new Error("Wrong Item type");
+ return item.roll();
+ }
+}
diff --git a/module/actor/sheets/newSheet/base.js b/module/actor/sheets/newSheet/base.js
index d6636c0b..0b2136bc 100644
--- a/module/actor/sheets/newSheet/base.js
+++ b/module/actor/sheets/newSheet/base.js
@@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
-import {SW5E} from '../../../config.js';
+import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
@@ -14,963 +14,976 @@ import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effe
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
- constructor(...args) {
- super(...args);
+ constructor(...args) {
+ super(...args);
+
+ /**
+ * Track the set of item filters which are applied
+ * @type {Set}
+ */
+ this._filters = {
+ inventory: new Set(),
+ forcePowerbook: new Set(),
+ techPowerbook: new Set(),
+ features: new Set(),
+ effects: new Set()
+ };
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ scrollY: [
+ ".inventory .group-list",
+ ".features .group-list",
+ ".force-powerbook .group-list",
+ ".tech-powerbook .group-list",
+ ".effects .effects-list"
+ ],
+ tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
+ });
+ }
+
+ /* -------------------------------------------- */
/**
- * Track the set of item filters which are applied
- * @type {Set}
+ * A set of item types that should be prevented from being dropped on this type of actor sheet.
+ * @type {Set}
*/
- this._filters = {
- inventory: new Set(),
- forcePowerbook: new Set(),
- techPowerbook: new Set(),
- features: new Set(),
- effects: new Set()
- };
- }
+ static unsupportedItemTypes = new Set();
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- scrollY: [
- ".inventory .group-list",
- ".features .group-list",
- ".force-powerbook .group-list",
- ".tech-powerbook .group-list",
- ".effects .effects-list"
- ],
- tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * A set of item types that should be prevented from being dropped on this type of actor sheet.
- * @type {Set}
- */
- static unsupportedItemTypes = new Set();
-
- /* -------------------------------------------- */
-
- /** @override */
- get template() {
- if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
- return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData(options) {
-
- // Basic data
- let isOwner = this.actor.isOwner;
- const data = {
- owner: isOwner,
- limited: this.actor.limited,
- options: this.options,
- editable: this.isEditable,
- cssClass: isOwner ? "editable" : "locked",
- isCharacter: this.actor.type === "character",
- isNPC: this.actor.type === "npc",
- isStarship: this.actor.type === "starship",
- isVehicle: this.actor.type === 'vehicle',
- config: CONFIG.SW5E,
- rollData: this.actor.getRollData.bind(this.actor)
- };
-
- // The Actor's data
- const actorData = this.actor.data.toObject(false);
- data.actor = actorData;
- data.data = actorData.data;
-
- // Owned Items
- data.items = actorData.items;
- for ( let i of data.items ) {
- const item = this.actor.items.get(i._id);
- i.labels = item.labels;
- }
- data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
-
- // Labels and filters
- data.labels = this.actor.labels || {};
- data.filters = this._filters;
-
- // Ability Scores
- for ( let [a, abl] of Object.entries(actorData.data.abilities)) {
- abl.icon = this._getProficiencyIcon(abl.proficient);
- abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
- abl.label = CONFIG.SW5E.abilities[a];
+ /** @override */
+ get template() {
+ if (!game.user.isGM && this.actor.limited)
+ return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
+ return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
}
- // Skills
- if (actorData.data.skills) {
- for (let [s, skl] of Object.entries(actorData.data.skills)) {
- skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
- skl.icon = this._getProficiencyIcon(skl.value);
- skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
- if (data.actor.type === "starship") {
- skl.label = CONFIG.SW5E.starshipSkills[s];
- }else{
- skl.label = CONFIG.SW5E.skills[s];
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData(options) {
+ // Basic data
+ let isOwner = this.actor.isOwner;
+ const data = {
+ owner: isOwner,
+ limited: this.actor.limited,
+ options: this.options,
+ editable: this.isEditable,
+ cssClass: isOwner ? "editable" : "locked",
+ isCharacter: this.actor.type === "character",
+ isNPC: this.actor.type === "npc",
+ isStarship: this.actor.type === "starship",
+ isVehicle: this.actor.type === "vehicle",
+ config: CONFIG.SW5E,
+ rollData: this.actor.getRollData.bind(this.actor)
+ };
+
+ // The Actor's data
+ const actorData = this.actor.data.toObject(false);
+ data.actor = actorData;
+ data.data = actorData.data;
+
+ // Owned Items
+ data.items = actorData.items;
+ for (let i of data.items) {
+ const item = this.actor.items.get(i._id);
+ i.labels = item.labels;
}
- }
- }
+ data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
- // Movement speeds
- data.movement = this._getMovementSpeed(actorData);
+ // Labels and filters
+ data.labels = this.actor.labels || {};
+ data.filters = this._filters;
- // Senses
- data.senses = this._getSenses(actorData);
-
- // Update traits
- this._prepareTraits(actorData.data.traits);
-
- // Prepare owned items
- this._prepareItems(data);
-
- // Prepare active effects
- data.effects = prepareActiveEffectCategories(this.actor.effects);
-
- // Return data to the sheet
- return data
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare the display of movement speed data for the Actor*
- * @param {object} actorData The Actor data being prepared.
- * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
- * @returns {{primary: string, special: string}}
- * @private
- */
- _getMovementSpeed(actorData, largestPrimary=false) {
- const movement = actorData.data.attributes.movement || {};
-
- // Prepare an array of available movement speeds
- let 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}`]
- ]
- if ( largestPrimary ) {
- speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
- }
-
- // Filter and sort speeds on their values
- speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
-
- // Case 1: Largest as primary
- if ( largestPrimary ) {
- let primary = speeds.shift();
- return {
- primary: `${primary ? primary[1] : "0"} ${movement.units}`,
- special: speeds.map(s => s[1]).join(", ")
- }
- }
-
- // Case 2: Walk as primary
- else {
- return {
- primary: `${movement.walk || 0} ${movement.units}`,
- special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
- }
- }
- }
-
- /* -------------------------------------------- */
-
- _getSenses(actorData) {
- const senses = actorData.data.attributes.senses || {};
- const tags = {};
- for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
- const v = senses[k] ?? 0
- if ( v === 0 ) continue;
- tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
- }
- if ( !!senses.special ) tags["special"] = senses.special;
- return tags;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
- * @param {object} traits The raw traits data object from the actor data
- * @private
- */
- _prepareTraits(traits) {
- const map = {
- "dr": CONFIG.SW5E.damageResistanceTypes,
- "di": CONFIG.SW5E.damageResistanceTypes,
- "dv": CONFIG.SW5E.damageResistanceTypes,
- "ci": CONFIG.SW5E.conditionTypes,
- "languages": CONFIG.SW5E.languages,
- "armorProf": CONFIG.SW5E.armorProficiencies,
- "weaponProf": CONFIG.SW5E.weaponProficiencies,
- "toolProf": CONFIG.SW5E.toolProficiencies
- };
- for ( let [t, choices] of Object.entries(map) ) {
- const trait = traits[t];
- if ( !trait ) continue;
- let values = [];
- if ( trait.value ) {
- values = trait.value instanceof Array ? trait.value : [trait.value];
- }
- trait.selected = values.reduce((obj, t) => {
- obj[t] = choices[t];
- return obj;
- }, {});
-
- // Add custom entry
- if ( trait.custom ) {
- trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
- }
- trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Insert a power into the powerbook object when rendering the character sheet
- * @param {Object} data The Actor data being prepared
- * @param {Array} powers The power data being prepared
- * @param {string} school The school of the powerbook being prepared
- * @private
- */
- _preparePowerbook(data, powers, school) {
- const owner = this.actor.isOwner;
- const levels = data.data.powers;
- const powerbook = {};
-
- // Define some mappings
- const sections = {
- "atwill": -20,
- "innate": -10,
- };
-
- // Label power slot uses headers
- const useLabels = {
- "-20": "-",
- "-10": "-",
- "0": "∞"
- };
-
- // Format a powerbook entry for a certain indexed level
- const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
- powerbook[i] = {
- order: i,
- label: label,
- usesSlots: i > 0,
- canCreate: owner,
- canPrepare: (data.actor.type === "character") && (i >= 1),
- powers: [],
- 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, "school": school},
- prop: sl
- };
- };
-
- // Determine the maximum power level which has a slot
- const maxLevel = Array.fromRange(10).reduce((max, i) => {
- if ( i === 0 ) return max;
- const level = levels[`power${i}`];
- if ( (level.max || level.override ) && ( i > max ) ) max = i;
- return max;
- }, 0);
-
- // 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++) {
- const sl = `power${lvl}`;
- registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
- }
- }
-
- // Iterate over every power item, adding powers to the powerbook by section
- powers.forEach(power => {
- const mode = power.data.preparation.mode || "prepared";
- let s = power.data.level || 0;
- const sl = `power${s}`;
-
- // Specialized powercasting modes (if they exist)
- if ( mode in sections ) {
- s = sections[mode];
- if ( !powerbook[s] ){
- 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
- });
+ // Ability Scores
+ for (let [a, abl] of Object.entries(actorData.data.abilities)) {
+ abl.icon = this._getProficiencyIcon(abl.proficient);
+ abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
+ abl.label = CONFIG.SW5E.abilities[a];
}
- }
- // 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: levels[sl]});
- }
-
- // Add the power to the relevant heading
- powerbook[s].powers.push(power);
- });
-
- // Sort the powerbook by section level
- const sorted = Object.values(powerbook);
- sorted.sort((a, b) => a.order - b.order);
- return sorted;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Determine whether an Owned Item will be shown based on the current set of filters
- * @return {boolean}
- * @private
- */
- _filterItems(items, filters) {
- return items.filter(item => {
- const data = item.data;
-
- // Action usage
- for ( let f of ["action", "bonus", "reaction"] ) {
- if ( filters.has(f) ) {
- if ((data.activation && (data.activation.type !== f))) return false;
+ // Skills
+ if (actorData.data.skills) {
+ for (let [s, skl] of Object.entries(actorData.data.skills)) {
+ skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
+ skl.icon = this._getProficiencyIcon(skl.value);
+ skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
+ if (data.actor.type === "starship") {
+ skl.label = CONFIG.SW5E.starshipSkills[s];
+ } else {
+ skl.label = CONFIG.SW5E.skills[s];
+ }
+ }
}
- }
- // Power-specific filters
- if ( filters.has("ritual") ) {
- if (data.components.ritual !== true) return false;
- }
- if ( filters.has("concentration") ) {
- if (data.components.concentration !== true) return false;
- }
- if ( filters.has("prepared") ) {
- if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
- if ( this.actor.data.type === "npc" ) return true;
- if ( this.actor.data.type === "starship" ) return true;
- return data.preparation.prepared;
- }
+ // Movement speeds
+ data.movement = this._getMovementSpeed(actorData);
- // Equipment-specific filters
- if ( filters.has("equipped") ) {
- if ( data.equipped !== true ) return false;
- }
- return true;
- });
- }
+ // Senses
+ data.senses = this._getSenses(actorData);
- /* -------------------------------------------- */
+ // Update traits
+ this._prepareTraits(actorData.data.traits);
- /**
- * Get the font-awesome icon used to display a certain level of skill proficiency
- * @private
- */
- _getProficiencyIcon(level) {
- const icons = {
- 0: ' ',
- 0.5: ' ',
- 1: ' ',
- 2: ' '
- };
- return icons[level] || icons[0];
- }
+ // Prepare owned items
+ this._prepareItems(data);
- /* -------------------------------------------- */
- /* Event Listeners and Handlers
- /* -------------------------------------------- */
+ // Prepare active effects
+ data.effects = prepareActiveEffectCategories(this.actor.effects);
- /** @inheritdoc */
- activateListeners(html) {
-
- // Activate Item Filters
- const filterLists = html.find(".filter-list");
- filterLists.each(this._initializeFilterItemList.bind(this));
- filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
-
- // Item summaries
- html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
-
- // View Item Sheets
- html.find('.item-edit').click(this._onItemEdit.bind(this));
-
- // Editable Only Listeners
- if ( this.isEditable ) {
-
- // Input focus and update
- const inputs = html.find("input");
- inputs.focus(ev => ev.currentTarget.select());
- inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
-
- // Ability Proficiency
- html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
-
- // Toggle Skill Proficiency
- html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
-
- // Trait Selector
- html.find('.trait-selector').click(this._onTraitSelector.bind(this));
-
- // Configure Special Flags
- html.find('.config-button').click(this._onConfigMenu.bind(this));
-
- // Owned Item management
- html.find('.item-create').click(this._onItemCreate.bind(this));
- html.find('.item-delete').click(this._onItemDelete.bind(this));
- html.find('.item-collapse').click(this._onItemCollapse.bind(this));
- html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
- html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
- html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this));
- html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this));
-
- // Active Effect management
- html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
+ // Return data to the sheet
+ return data;
}
- // Owner Only Listeners
- if ( this.actor.isOwner ) {
+ /* -------------------------------------------- */
- // Ability Checks
- html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
+ /**
+ * Prepare the display of movement speed data for the Actor*
+ * @param {object} actorData The Actor data being prepared.
+ * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
+ * @returns {{primary: string, special: string}}
+ * @private
+ */
+ _getMovementSpeed(actorData, largestPrimary = false) {
+ const movement = actorData.data.attributes.movement || {};
-
- // Roll Skill Checks
- html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
-
- // Item Rolling
- html.find('.item .item-image').click(event => this._onItemRoll(event));
- html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
- }
-
- // Otherwise remove rollable classes
- else {
- html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
- }
-
- // Handle default listeners last so system listeners are triggered first
- super.activateListeners(html);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Iinitialize Item list filters by activating the set of filters which are currently applied
- * @private
- */
- _initializeFilterItemList(i, ul) {
- const set = this._filters[ul.dataset.filter];
- const filters = ul.querySelectorAll(".filter-item");
- for ( let li of filters ) {
- if ( set.has(li.dataset.filter) ) li.classList.add("active");
- }
- }
-
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
-
- /**
- * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
- * @param event
- * @private
- */
- _onChangeInputDelta(event) {
- const input = event.target;
- const value = input.value;
- if ( ["+", "-"].includes(value[0]) ) {
- let delta = parseFloat(value);
- input.value = getProperty(this.actor.data, input.name) + delta;
- } else if ( value[0] === "=" ) {
- input.value = value.slice(1);
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
- * @param {Event} event The click event which originated the selection
- * @private
- */
- _onConfigMenu(event) {
- event.preventDefault();
- const button = event.currentTarget;
- let app;
- switch ( button.dataset.action ) {
- case "hit-dice":
- app = new ActorHitDiceConfig(this.object);
- break;
- case "movement":
- app = new ActorMovementConfig(this.object);
- break;
- case "flags":
- app = new ActorSheetFlags(this.object);
- break;
- case "senses":
- app = new ActorSensesConfig(this.object);
- break;
- case "type":
- new ActorTypeConfig(this.object).render(true);
- break;
- }
- app?.render(true);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle cycling proficiency in a Skill
- * @param {Event} event A click or contextmenu event which triggered the handler
- * @private
- */
- _onCycleSkillProficiency(event) {
- event.preventDefault();
- const field = $(event.currentTarget).siblings('input[type="hidden"]');
-
- // Get the current level and the array of levels
- const level = parseFloat(field.val());
- const levels = [0, 1, 0.5, 2];
- let idx = levels.indexOf(level);
-
- // Toggle next level - forward on click, backwards on right
- if ( event.type === "click" ) {
- field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
- } else if ( event.type === "contextmenu" ) {
- field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
- }
-
- // Update the field value and save the form
- this._onSubmit(event);
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async _onDropActor(event, data) {
- const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
- if ( !canPolymorph ) return false;
-
- // Get the target actor
- let sourceActor = null;
- if (data.pack) {
- const pack = game.packs.find(p => p.collection === data.pack);
- sourceActor = await pack.getEntity(data.id);
- } else {
- sourceActor = game.actors.get(data.id);
- }
- if ( !sourceActor ) return;
-
- // Define a function to record polymorph settings for future use
- const rememberOptions = html => {
- const options = {};
- html.find('input').each((i, el) => {
- options[el.name] = el.checked;
- });
- const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
- game.settings.set('sw5e', 'polymorphSettings', settings);
- return settings;
- };
-
- // Create and render the Dialog
- return new Dialog({
- title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
- content: {
- options: game.settings.get('sw5e', 'polymorphSettings'),
- i18n: SW5E.polymorphSettings,
- isToken: this.actor.isToken
- },
- default: 'accept',
- buttons: {
- accept: {
- icon: ' ',
- label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
- callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
- },
- wildshape: {
- icon: ' ',
- label: game.i18n.localize('SW5E.PolymorphWildShape'),
- callback: html => this.actor.transformInto(sourceActor, {
- keepBio: true,
- keepClass: true,
- keepMental: true,
- mergeSaves: true,
- mergeSkills: true,
- transformTokens: rememberOptions(html).transformTokens
- })
- },
- polymorph: {
- icon: ' ',
- label: game.i18n.localize('SW5E.Polymorph'),
- callback: html => this.actor.transformInto(sourceActor, {
- transformTokens: rememberOptions(html).transformTokens
- })
- },
- cancel: {
- icon: ' ',
- label: game.i18n.localize('Cancel')
+ // Prepare an array of available movement speeds
+ let 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}`]
+ ];
+ if (largestPrimary) {
+ speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
}
- }
- }, {
- classes: ['dialog', 'sw5e'],
- width: 600,
- template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
- }).render(true);
- }
- /* -------------------------------------------- */
+ // Filter and sort speeds on their values
+ speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
- /** @override */
- async _onDropItemCreate(itemData) {
+ // Case 1: Largest as primary
+ if (largestPrimary) {
+ let primary = speeds.shift();
+ return {
+ primary: `${primary ? primary[1] : "0"} ${movement.units}`,
+ special: speeds.map((s) => s[1]).join(", ")
+ };
+ }
- // Check to make sure items of this type are allowed on this actor
- if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
- return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
- itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
- actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
- }));
+ // Case 2: Walk as primary
+ else {
+ return {
+ primary: `${movement.walk || 0} ${movement.units}`,
+ special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
+ };
+ }
}
- // Create a Consumable power scroll on the Inventory tab
- if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
- const scroll = await Item5e.createScrollFromPower(itemData);
- itemData = scroll.data;
+ /* -------------------------------------------- */
+
+ _getSenses(actorData) {
+ const senses = actorData.data.attributes.senses || {};
+ const tags = {};
+ for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
+ const v = senses[k] ?? 0;
+ if (v === 0) continue;
+ tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
+ }
+ if (!!senses.special) tags["special"] = senses.special;
+ return tags;
}
- if ( itemData.data ) {
- // Ignore certain statuses
- ["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
+ /* -------------------------------------------- */
- // Downgrade ATTUNED to REQUIRED
- itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
+ /**
+ * 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
+ * @private
+ */
+ _prepareTraits(traits) {
+ const map = {
+ dr: CONFIG.SW5E.damageResistanceTypes,
+ di: CONFIG.SW5E.damageResistanceTypes,
+ dv: CONFIG.SW5E.damageResistanceTypes,
+ ci: CONFIG.SW5E.conditionTypes,
+ languages: CONFIG.SW5E.languages,
+ armorProf: CONFIG.SW5E.armorProficiencies,
+ weaponProf: CONFIG.SW5E.weaponProficiencies,
+ toolProf: CONFIG.SW5E.toolProficiencies
+ };
+ for (let [t, choices] of Object.entries(map)) {
+ const trait = traits[t];
+ if (!trait) continue;
+ let values = [];
+ if (trait.value) {
+ values = trait.value instanceof Array ? trait.value : [trait.value];
+ }
+ trait.selected = values.reduce((obj, t) => {
+ obj[t] = choices[t];
+ return obj;
+ }, {});
+
+ // Add custom entry
+ if (trait.custom) {
+ trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
+ }
+ trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
+ }
}
- // Stack identical consumables
- if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
- const similarItem = this.actor.items.find(i => {
- const sourceId = i.getFlag("core", "sourceId");
- return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
- (i.type === "consumable");
- });
- if ( similarItem && itemData.name !== "Power Cell" ) { // Always create a new powercell instead of increasing quantity
- return similarItem.update({
- 'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
+ /* -------------------------------------------- */
+
+ /**
+ * Insert a power into the powerbook object when rendering the character sheet
+ * @param {Object} data The Actor data being prepared
+ * @param {Array} powers The power data being prepared
+ * @param {string} school The school of the powerbook being prepared
+ * @private
+ */
+ _preparePowerbook(data, powers, school) {
+ const owner = this.actor.isOwner;
+ const levels = data.data.powers;
+ const powerbook = {};
+
+ // Define some mappings
+ const sections = {
+ atwill: -20,
+ innate: -10
+ };
+
+ // Label power slot uses headers
+ const useLabels = {
+ "-20": "-",
+ "-10": "-",
+ "0": "∞"
+ };
+
+ // Format a powerbook entry for a certain indexed level
+ const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
+ powerbook[i] = {
+ order: i,
+ label: label,
+ usesSlots: i > 0,
+ canCreate: owner,
+ canPrepare: data.actor.type === "character" && i >= 1,
+ powers: [],
+ 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,
+ "school": school
+ },
+ prop: sl
+ };
+ };
+
+ // Determine the maximum power level which has a slot
+ const maxLevel = Array.fromRange(10).reduce((max, i) => {
+ if (i === 0) return max;
+ const level = levels[`power${i}`];
+ if ((level.max || level.override) && i > max) max = i;
+ return max;
+ }, 0);
+
+ // 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++) {
+ const sl = `power${lvl}`;
+ registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
+ }
+ }
+
+ // Iterate over every power item, adding powers to the powerbook by section
+ powers.forEach((power) => {
+ const mode = power.data.preparation.mode || "prepared";
+ let s = power.data.level || 0;
+ const sl = `power${s}`;
+
+ // Specialized powercasting modes (if they exist)
+ if (mode in sections) {
+ s = sections[mode];
+ if (!powerbook[s]) {
+ 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
+ });
+ }
+ }
+
+ // 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: levels[sl]});
+ }
+
+ // Add the power to the relevant heading
+ powerbook[s].powers.push(power);
});
- }
+
+ // Sort the powerbook by section level
+ const sorted = Object.values(powerbook);
+ sorted.sort((a, b) => a.order - b.order);
+ return sorted;
}
- // Create the owned item as normal
- return super._onDropItemCreate(itemData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Determine whether an Owned Item will be shown based on the current set of filters
+ * @return {boolean}
+ * @private
+ */
+ _filterItems(items, filters) {
+ return items.filter((item) => {
+ const data = item.data;
- /**
- * Handle enabling editing for a power slot override value
- * @param {MouseEvent} event The originating click event
- * @private
- */
- async _onPowerSlotOverride (event) {
- const span = event.currentTarget.parentElement;
- const level = span.dataset.level;
- const override = this.actor.data.data.powers[level].override || span.dataset.slots;
- const input = document.createElement("INPUT");
- input.type = "text";
- input.name = `data.powers.${level}.override`;
- input.value = override;
- input.placeholder = span.dataset.slots;
- input.dataset.dtype = "Number";
+ // Action usage
+ for (let f of ["action", "bonus", "reaction"]) {
+ if (filters.has(f)) {
+ if (data.activation && data.activation.type !== f) return false;
+ }
+ }
- // Replace the HTML
- const parent = span.parentElement;
- parent.removeChild(span);
- parent.appendChild(input);
- }
+ // Power-specific filters
+ if (filters.has("ritual")) {
+ if (data.components.ritual !== true) return false;
+ }
+ if (filters.has("concentration")) {
+ if (data.components.concentration !== true) return false;
+ }
+ if (filters.has("prepared")) {
+ if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
+ if (this.actor.data.type === "npc") return true;
+ if (this.actor.data.type === "starship") return true;
+ return data.preparation.prepared;
+ }
- /* -------------------------------------------- */
-
- /**
- * Change the uses amount of an Owned Item within the Actor
- * @param {Event} event The triggering click event
- * @private
- */
- async _onUsesChange(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(itemId);
- const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
- event.target.value = uses;
- return item.update({ 'data.uses.value': uses });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
- * @private
- */
- _onItemRoll(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(itemId);
- return item.roll();
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle attempting to recharge an item usage by rolling a recharge check
- * @param {Event} event The originating click event
- * @private
- */
- _onItemRecharge(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(itemId);
- return item.rollRecharge();
- };
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
- * @private
- */
- _onItemSummary(event) {
- event.preventDefault();
- let li = $(event.currentTarget).parents(".item"),
- item = this.actor.items.get(li.data("item-id")),
- chatData = item.getChatData({secrets: this.actor.isOwner});
-
- // Toggle summary
- if ( li.hasClass("expanded") ) {
- let summary = li.children(".item-summary");
- summary.slideUp(200, () => summary.remove());
- } else {
- let div = $(`${chatData.description.value}
`);
- let props = $(`
`);
- chatData.properties.forEach(p => props.append(`${p} `));
- div.append(props);
- li.append(div.hide());
- div.slideDown(200);
+ // Equipment-specific filters
+ if (filters.has("equipped")) {
+ if (data.equipped !== true) return false;
+ }
+ return true;
+ });
}
- li.toggleClass("expanded");
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
- * @param {Event} event The originating click event
- * @private
- */
- _onItemCreate(event) {
- event.preventDefault();
- const header = event.currentTarget;
- const type = header.dataset.type;
- const itemData = {
- name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
- type: type,
- data: foundry.utils.deepClone(header.dataset)
- };
- delete itemData.data["type"];
- return this.actor.createEmbeddedDocuments("Item", [itemData]);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle editing an existing Owned Item for the Actor
- * @param {Event} event The originating click event
- * @private
- */
- _onItemEdit(event) {
- event.preventDefault();
- const li = event.currentTarget.closest(".item");
- const item = this.actor.items.get(li.dataset.itemId);
- return item.sheet.render(true);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle deleting an existing Owned Item for the Actor
- * @param {Event} event The originating click event
- * @private
- */
- _onItemDelete(event) {
- event.preventDefault();
- const li = event.currentTarget.closest(".item");
- const item = this.actor.items.get(li.dataset.itemId);
- if ( item ) return item.delete();
- }
-
- /**
- * Handle collapsing a Feature row on the actor sheet
- * @param {Event} event The originating click event
- * @private
- */
-
-_onItemCollapse(event) {
- event.preventDefault();
-
- event.currentTarget.classList.toggle("active");
-
- const li = event.currentTarget.closest("li");
- const content = li.querySelector(".content");
-
- if (content.style.display === "none") {
- content.style.display = "block";
- } else {
- content.style.display = "none";
+ /**
+ * Get the font-awesome icon used to display a certain level of skill proficiency
+ * @private
+ */
+ _getProficiencyIcon(level) {
+ const icons = {
+ 0: ' ',
+ 0.5: ' ',
+ 1: ' ',
+ 2: ' '
+ };
+ return icons[level] || icons[0];
}
- }
-/**
- * Handle incrementing class level on the actor sheet
- * @param {Event} event The originating click event
- * @private
- */
-
- _onIncrementClassLevel(event) {
- event.preventDefault();
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers
+ /* -------------------------------------------- */
- const div = event.currentTarget.closest(".character")
- const li = event.currentTarget.closest("li");
+ /** @inheritdoc */
+ activateListeners(html) {
+ // Activate Item Filters
+ const filterLists = html.find(".filter-list");
+ filterLists.each(this._initializeFilterItemList.bind(this));
+ filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
- const actorId = div.id.split("-")[1];
- const itemId = li.dataset.itemId;
+ // Item summaries
+ html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
- const actor = game.actors.get(actorId);
- const item = actor.items.get(itemId);
+ // View Item Sheets
+ html.find(".item-edit").click(this._onItemEdit.bind(this));
- let levels = item.data.data.levels;
- const update = {_id: item.data._id, data: {levels: (levels + 1) }};
+ // Editable Only Listeners
+ if (this.isEditable) {
+ // Input focus and update
+ const inputs = html.find("input");
+ inputs.focus((ev) => ev.currentTarget.select());
+ inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
- actor.updateEmbeddedDocuments("Item", [update]);
-}
-
-/**
- * Handle decrementing class level on the actor sheet
- * @param {Event} event The originating click event
- * @private
- */
-
- _onDecrementClassLevel(event) {
- event.preventDefault();
-
- const div = event.currentTarget.closest(".character")
- const li = event.currentTarget.closest("li");
-
- const actorId = div.id.split("-")[1];
- const itemId = li.dataset.itemId;
-
- const actor = game.actors.get(actorId);
- const item = actor.items.get(itemId);
-
- let levels = item.data.data.levels;
- const update = {_id: item.data._id, data: {levels: (levels - 1) }};
-
- actor.updateEmbeddedDocuments("Item", [update]);
-}
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling an Ability check, either a test or a saving throw
- * @param {Event} event The originating click event
- * @private
- */
- _onRollAbilityTest(event) {
- event.preventDefault();
- let ability = event.currentTarget.parentElement.dataset.ability;
- return this.actor.rollAbility(ability, {event: event});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling a Skill check
- * @param {Event} event The originating click event
- * @private
- */
- _onRollSkillCheck(event) {
- event.preventDefault();
- const skill = event.currentTarget.parentElement.dataset.skill;
- return this.actor.rollSkill(skill, {event: event});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle toggling Ability score proficiency level
- * @param {Event} event The originating click event
- * @private
- */
- _onToggleAbilityProficiency(event) {
- event.preventDefault();
- const field = event.currentTarget.previousElementSibling;
- return this.actor.update({[field.name]: 1 - parseInt(field.value)});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle toggling of filters to display a different set of owned items
- * @param {Event} event The click event which triggered the toggle
- * @private
- */
- _onToggleFilter(event) {
- event.preventDefault();
- const li = event.currentTarget;
- const set = this._filters[li.parentElement.dataset.filter];
- const filter = li.dataset.filter;
- if ( set.has(filter) ) set.delete(filter);
- else set.add(filter);
- return this.render();
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
- * @param {Event} event The click event which originated the selection
- * @private
- */
- _onTraitSelector(event) {
- event.preventDefault();
- const a = event.currentTarget;
- const label = a.parentElement.querySelector("label");
- const choices = CONFIG.SW5E[a.dataset.options];
- const options = { name: a.dataset.target, title: label.innerText, choices };
- return new TraitSelector(this.actor, options).render(true)
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- _getHeaderButtons() {
- let buttons = super._getHeaderButtons();
- if (this.actor.isPolymorphed) {
- buttons.unshift({
- label: 'SW5E.PolymorphRestoreTransformation',
- class: "restore-transformation",
- icon: "fas fa-backward",
- onclick: () => this.actor.revertOriginalForm()
- });
- }
- return buttons;
- }
+ // Ability Proficiency
+ html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
+
+ // Toggle Skill Proficiency
+ html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
+
+ // Trait Selector
+ html.find(".trait-selector").click(this._onTraitSelector.bind(this));
+
+ // Configure Special Flags
+ html.find(".config-button").click(this._onConfigMenu.bind(this));
+
+ // Owned Item management
+ html.find(".item-create").click(this._onItemCreate.bind(this));
+ html.find(".item-delete").click(this._onItemDelete.bind(this));
+ html.find(".item-collapse").click(this._onItemCollapse.bind(this));
+ html.find(".item-uses input")
+ .click((ev) => ev.target.select())
+ .change(this._onUsesChange.bind(this));
+ html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
+ html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this));
+ html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
+
+ // Active Effect management
+ html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
+ }
+
+ // Owner Only Listeners
+ if (this.actor.isOwner) {
+ // Ability Checks
+ html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
+
+ // Roll Skill Checks
+ html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
+
+ // Item Rolling
+ html.find(".item .item-image").click((event) => this._onItemRoll(event));
+ html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
+ }
+
+ // Otherwise remove rollable classes
+ else {
+ html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
+ }
+
+ // Handle default listeners last so system listeners are triggered first
+ super.activateListeners(html);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Iinitialize Item list filters by activating the set of filters which are currently applied
+ * @private
+ */
+ _initializeFilterItemList(i, ul) {
+ const set = this._filters[ul.dataset.filter];
+ const filters = ul.querySelectorAll(".filter-item");
+ for (let li of filters) {
+ if (set.has(li.dataset.filter)) li.classList.add("active");
+ }
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /**
+ * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
+ * @param event
+ * @private
+ */
+ _onChangeInputDelta(event) {
+ const input = event.target;
+ const value = input.value;
+ if (["+", "-"].includes(value[0])) {
+ let delta = parseFloat(value);
+ input.value = getProperty(this.actor.data, input.name) + delta;
+ } else if (value[0] === "=") {
+ input.value = value.slice(1);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onConfigMenu(event) {
+ event.preventDefault();
+ const button = event.currentTarget;
+ let app;
+ switch (button.dataset.action) {
+ case "hit-dice":
+ app = new ActorHitDiceConfig(this.object);
+ break;
+ case "movement":
+ app = new ActorMovementConfig(this.object);
+ break;
+ case "flags":
+ app = new ActorSheetFlags(this.object);
+ break;
+ case "senses":
+ app = new ActorSensesConfig(this.object);
+ break;
+ case "type":
+ new ActorTypeConfig(this.object).render(true);
+ break;
+ }
+ app?.render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle cycling proficiency in a Skill
+ * @param {Event} event A click or contextmenu event which triggered the handler
+ * @private
+ */
+ _onCycleSkillProficiency(event) {
+ event.preventDefault();
+ const field = $(event.currentTarget).siblings('input[type="hidden"]');
+
+ // Get the current level and the array of levels
+ const level = parseFloat(field.val());
+ const levels = [0, 1, 0.5, 2];
+ let idx = levels.indexOf(level);
+
+ // Toggle next level - forward on click, backwards on right
+ if (event.type === "click") {
+ field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
+ } else if (event.type === "contextmenu") {
+ field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
+ }
+
+ // Update the field value and save the form
+ this._onSubmit(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropActor(event, data) {
+ const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
+ if (!canPolymorph) return false;
+
+ // Get the target actor
+ let sourceActor = null;
+ if (data.pack) {
+ const pack = game.packs.find((p) => p.collection === data.pack);
+ sourceActor = await pack.getEntity(data.id);
+ } else {
+ sourceActor = game.actors.get(data.id);
+ }
+ if (!sourceActor) return;
+
+ // Define a function to record polymorph settings for future use
+ const rememberOptions = (html) => {
+ const options = {};
+ html.find("input").each((i, el) => {
+ options[el.name] = el.checked;
+ });
+ const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
+ game.settings.set("sw5e", "polymorphSettings", settings);
+ return settings;
+ };
+
+ // Create and render the Dialog
+ return new Dialog(
+ {
+ title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
+ content: {
+ options: game.settings.get("sw5e", "polymorphSettings"),
+ i18n: SW5E.polymorphSettings,
+ isToken: this.actor.isToken
+ },
+ default: "accept",
+ buttons: {
+ accept: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
+ callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
+ },
+ wildshape: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.PolymorphWildShape"),
+ callback: (html) =>
+ this.actor.transformInto(sourceActor, {
+ keepBio: true,
+ keepClass: true,
+ keepMental: true,
+ mergeSaves: true,
+ mergeSkills: true,
+ transformTokens: rememberOptions(html).transformTokens
+ })
+ },
+ polymorph: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.Polymorph"),
+ callback: (html) =>
+ this.actor.transformInto(sourceActor, {
+ transformTokens: rememberOptions(html).transformTokens
+ })
+ },
+ cancel: {
+ icon: ' ',
+ label: game.i18n.localize("Cancel")
+ }
+ }
+ },
+ {
+ classes: ["dialog", "sw5e"],
+ width: 600,
+ template: "systems/sw5e/templates/apps/polymorph-prompt.html"
+ }
+ ).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+ // Check to make sure items of this type are allowed on this actor
+ if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
+ return ui.notifications.warn(
+ game.i18n.format("SW5E.ActorWarningInvalidItem", {
+ itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
+ actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
+ })
+ );
+ }
+
+ // Create a Consumable power scroll on the Inventory tab
+ if (itemData.type === "power" && this._tabs[0].active === "inventory") {
+ const scroll = await Item5e.createScrollFromPower(itemData);
+ itemData = scroll.data;
+ }
+
+ if (itemData.data) {
+ // Ignore certain statuses
+ ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
+
+ // Downgrade ATTUNED to REQUIRED
+ itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
+ }
+
+ // Stack identical consumables
+ if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
+ const similarItem = this.actor.items.find((i) => {
+ const sourceId = i.getFlag("core", "sourceId");
+ return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
+ });
+ if (similarItem && itemData.name !== "Power Cell") {
+ // Always create a new powercell instead of increasing quantity
+ return similarItem.update({
+ "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
+ });
+ }
+ }
+
+ // Create the owned item as normal
+ return super._onDropItemCreate(itemData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle enabling editing for a power slot override value
+ * @param {MouseEvent} event The originating click event
+ * @private
+ */
+ async _onPowerSlotOverride(event) {
+ const span = event.currentTarget.parentElement;
+ const level = span.dataset.level;
+ const override = this.actor.data.data.powers[level].override || span.dataset.slots;
+ const input = document.createElement("INPUT");
+ input.type = "text";
+ input.name = `data.powers.${level}.override`;
+ input.value = override;
+ input.placeholder = span.dataset.slots;
+ input.dataset.dtype = "Number";
+
+ // Replace the HTML
+ const parent = span.parentElement;
+ parent.removeChild(span);
+ parent.appendChild(input);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Change the uses amount of an Owned Item within the Actor
+ * @param {Event} event The triggering click event
+ * @private
+ */
+ async _onUsesChange(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
+ event.target.value = uses;
+ return item.update({"data.uses.value": uses});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
+ * @private
+ */
+ _onItemRoll(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ return item.roll();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle attempting to recharge an item usage by rolling a recharge check
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemRecharge(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ return item.rollRecharge();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
+ * @private
+ */
+ _onItemSummary(event) {
+ event.preventDefault();
+ let li = $(event.currentTarget).parents(".item"),
+ item = this.actor.items.get(li.data("item-id")),
+ chatData = item.getChatData({secrets: this.actor.isOwner});
+
+ // Toggle summary
+ if (li.hasClass("expanded")) {
+ let summary = li.children(".item-summary");
+ summary.slideUp(200, () => summary.remove());
+ } else {
+ let div = $(`${chatData.description.value}
`);
+ let props = $(`
`);
+ chatData.properties.forEach((p) => props.append(`${p} `));
+ div.append(props);
+ li.append(div.hide());
+ div.slideDown(200);
+ }
+ li.toggleClass("expanded");
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemCreate(event) {
+ event.preventDefault();
+ const header = event.currentTarget;
+ const type = header.dataset.type;
+ const itemData = {
+ name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
+ type: type,
+ data: foundry.utils.deepClone(header.dataset)
+ };
+ delete itemData.data["type"];
+ return this.actor.createEmbeddedDocuments("Item", [itemData]);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle editing an existing Owned Item for the Actor
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemEdit(event) {
+ event.preventDefault();
+ const li = event.currentTarget.closest(".item");
+ const item = this.actor.items.get(li.dataset.itemId);
+ return item.sheet.render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle deleting an existing Owned Item for the Actor
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemDelete(event) {
+ event.preventDefault();
+ const li = event.currentTarget.closest(".item");
+ const item = this.actor.items.get(li.dataset.itemId);
+ if (item) return item.delete();
+ }
+
+ /**
+ * Handle collapsing a Feature row on the actor sheet
+ * @param {Event} event The originating click event
+ * @private
+ */
+
+ _onItemCollapse(event) {
+ event.preventDefault();
+
+ event.currentTarget.classList.toggle("active");
+
+ const li = event.currentTarget.closest("li");
+ const content = li.querySelector(".content");
+
+ if (content.style.display === "none") {
+ content.style.display = "block";
+ } else {
+ content.style.display = "none";
+ }
+ }
+
+ /**
+ * Handle incrementing class level on the actor sheet
+ * @param {Event} event The originating click event
+ * @private
+ */
+
+ _onIncrementClassLevel(event) {
+ event.preventDefault();
+
+ const div = event.currentTarget.closest(".character");
+ const li = event.currentTarget.closest("li");
+
+ const actorId = div.id.split("-")[1];
+ const itemId = li.dataset.itemId;
+
+ const actor = game.actors.get(actorId);
+ const item = actor.items.get(itemId);
+
+ let levels = item.data.data.levels;
+ const update = {_id: item.data._id, data: {levels: levels + 1}};
+
+ actor.updateEmbeddedDocuments("Item", [update]);
+ }
+
+ /**
+ * Handle decrementing class level on the actor sheet
+ * @param {Event} event The originating click event
+ * @private
+ */
+
+ _onDecrementClassLevel(event) {
+ event.preventDefault();
+
+ const div = event.currentTarget.closest(".character");
+ const li = event.currentTarget.closest("li");
+
+ const actorId = div.id.split("-")[1];
+ const itemId = li.dataset.itemId;
+
+ const actor = game.actors.get(actorId);
+ const item = actor.items.get(itemId);
+
+ let levels = item.data.data.levels;
+ const update = {_id: item.data._id, data: {levels: levels - 1}};
+
+ actor.updateEmbeddedDocuments("Item", [update]);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling an Ability check, either a test or a saving throw
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onRollAbilityTest(event) {
+ event.preventDefault();
+ let ability = event.currentTarget.parentElement.dataset.ability;
+ return this.actor.rollAbility(ability, {event: event});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling a Skill check
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onRollSkillCheck(event) {
+ event.preventDefault();
+ const skill = event.currentTarget.parentElement.dataset.skill;
+ return this.actor.rollSkill(skill, {event: event});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling Ability score proficiency level
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onToggleAbilityProficiency(event) {
+ event.preventDefault();
+ const field = event.currentTarget.previousElementSibling;
+ return this.actor.update({[field.name]: 1 - parseInt(field.value)});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling of filters to display a different set of owned items
+ * @param {Event} event The click event which triggered the toggle
+ * @private
+ */
+ _onToggleFilter(event) {
+ event.preventDefault();
+ const li = event.currentTarget;
+ const set = this._filters[li.parentElement.dataset.filter];
+ const filter = li.dataset.filter;
+ if (set.has(filter)) set.delete(filter);
+ else set.add(filter);
+ return this.render();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onTraitSelector(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+ const label = a.parentElement.querySelector("label");
+ const choices = CONFIG.SW5E[a.dataset.options];
+ const options = {name: a.dataset.target, title: label.innerText, choices};
+ return new TraitSelector(this.actor, options).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ _getHeaderButtons() {
+ let buttons = super._getHeaderButtons();
+ if (this.actor.isPolymorphed) {
+ buttons.unshift({
+ label: "SW5E.PolymorphRestoreTransformation",
+ class: "restore-transformation",
+ icon: "fas fa-backward",
+ onclick: () => this.actor.revertOriginalForm()
+ });
+ }
+ return buttons;
+ }
}
diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js
index 3390c482..91c68b1d 100644
--- a/module/actor/sheets/newSheet/character.js
+++ b/module/actor/sheets/newSheet/character.js
@@ -7,246 +7,339 @@ import Actor5e from "../../entity.js";
* @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"
+ }
+ ]
+ });
+ }
- 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;
- /**
- * Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
- */
- getData() {
- const sheetData = super.getData();
+ // 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]);
+ }, []);
- // Temporary HP
- let hp = sheetData.data.attributes.hp;
- if (hp.temp === 0) delete hp.temp;
- if (hp.tempmax === 0) delete hp.tempmax;
+ // Experience Tracking
+ sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
+ sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
+ sheetData["multiclassLabels"] = this.actor.itemTypes.class
+ .map((c) => {
+ return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
+ })
+ .join(", ");
- // 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]);
- }, []);
+ // Return data for rendering
+ return sheetData;
+ }
- // Experience Tracking
- sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
- sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
- sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
- return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
- }).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,
+ forcepowers,
+ techpowers,
+ feats,
+ classes,
+ deployments,
+ deploymentfeatures,
+ ventures,
+ species,
+ archetypes,
+ classfeatures,
+ backgrounds,
+ fightingstyles,
+ fightingmasteries,
+ lightsaberforms
+ ] = data.items.reduce(
+ (arr, item) => {
+ // Item details
+ item.img = item.img || CONST.DEFAULT_TOKEN;
+ item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
+ item.attunement = {
+ [CONFIG.SW5E.attunementTypes.REQUIRED]: {
+ icon: "fa-sun",
+ cls: "not-attuned",
+ title: "SW5E.AttunementRequired"
+ },
+ [CONFIG.SW5E.attunementTypes.ATTUNED]: {
+ icon: "fa-sun",
+ cls: "attuned",
+ title: "SW5E.AttunementAttuned"
+ }
+ }[item.data.attunement];
- /**
- * Organize and classify Owned Items for Character sheets
- * @private
- */
- _prepareItems(data) {
+ // 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);
- // 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"} }
- };
+ // Item toggle state
+ this._prepareItemToggleState(item);
- // Partition items by category
- let [items, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
+ // Primary Class
+ if (item.type === "class")
+ item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
- // Item details
- item.img = item.img || CONST.DEFAULT_TOKEN;
- item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
- item.attunement = {
- [CONFIG.SW5E.attunementTypes.REQUIRED]: {
- icon: "fa-sun",
- cls: "not-attuned",
- title: "SW5E.AttunementRequired"
- },
- [CONFIG.SW5E.attunementTypes.ATTUNED]: {
- icon: "fa-sun",
- cls: "attuned",
- title: "SW5E.AttunementAttuned"
+ // Classify items into types
+ if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item);
+ else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item);
+ else if (item.type === "feat") arr[3].push(item);
+ else if (item.type === "class") arr[4].push(item);
+ else if (item.type === "deployment") arr[5].push(item);
+ else if (item.type === "deploymentfeature") arr[6].push(item);
+ else if (item.type === "venture") arr[7].push(item);
+ else if (item.type === "species") arr[8].push(item);
+ else if (item.type === "archetype") arr[9].push(item);
+ else if (item.type === "classfeature") arr[10].push(item);
+ else if (item.type === "background") arr[11].push(item);
+ else if (item.type === "fightingstyle") arr[12].push(item);
+ else if (item.type === "fightingmastery") arr[13].push(item);
+ else if (item.type === "lightsaberform") arr[14].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);
+ forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
+ techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
+ 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 = (i.data.quantity * i.data.weight).toNearest(0.1);
+ inventory[i.type].items.push(i);
}
- }[item.data.attunement];
- // 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));
+ // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
+ const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
+ const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
- // Item toggle state
- this._prepareItemToggleState(item);
+ // Organize Features
+ const features = {
+ classes: {
+ label: "SW5E.ItemTypeClassPl",
+ items: [],
+ hasActions: false,
+ dataset: {type: "class"},
+ isClass: true
+ },
+ classfeatures: {
+ label: "SW5E.ItemTypeClassFeats",
+ items: [],
+ hasActions: true,
+ dataset: {type: "classfeature"},
+ isClassfeature: true
+ },
+ archetype: {
+ label: "SW5E.ItemTypeArchetype",
+ items: [],
+ hasActions: false,
+ dataset: {type: "archetype"},
+ isArchetype: true
+ },
+ deployments: {
+ label: "SW5E.ItemTypeDeploymentPl",
+ items: [],
+ hasActions: false,
+ dataset: {type: "deployment"},
+ isDeployment: true
+ },
+ deploymentfeatures: {
+ label: "SW5E.ItemTypeDeploymentFeaturePl",
+ items: [],
+ hasActions: true,
+ dataset: {type: "deploymentfeature"},
+ isDeploymentfeature: true
+ },
+ ventures: {
+ label: "SW5E.ItemTypeVenturePl",
+ items: [],
+ hasActions: false,
+ dataset: {type: "venture"},
+ isVenture: 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"}}
+ };
+ 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.data.levels - a.data.levels);
+ features.classes.items = classes;
+ features.classfeatures.items = classfeatures;
+ features.archetype.items = archetypes;
+ features.deployments.items = deployments;
+ features.deploymentfeatures.items = deploymentfeatures;
+ features.ventures.items = ventures;
+ features.species.items = species;
+ features.background.items = backgrounds;
+ features.fightingstyles.items = fightingstyles;
+ features.fightingmasteries.items = fightingmasteries;
+ features.lightsaberforms.items = lightsaberforms;
- // Primary Class
- if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
-
- // Classify items into types
- if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item);
- else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
- else if ( item.type === "feat" ) arr[3].push(item);
- else if ( item.type === "class" ) arr[4].push(item);
- else if ( item.type === "deployment" ) arr[5].push(item);
- else if ( item.type === "deploymentfeature" ) arr[6].push(item);
- else if ( item.type === "venture" ) arr[7].push(item);
- else if ( item.type === "species" ) arr[8].push(item);
- else if ( item.type === "archetype" ) arr[9].push(item);
- else if ( item.type === "classfeature" ) arr[10].push(item);
- else if ( item.type === "background" ) arr[11].push(item);
- else if ( item.type === "fightingstyle" ) arr[12].push(item);
- else if ( item.type === "fightingmastery" ) arr[13].push(item);
- else if ( item.type === "lightsaberform" ) arr[14].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);
- forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
- techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
- 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 = (i.data.quantity * i.data.weight).toNearest(0.1);
- inventory[i.type].items.push(i);
+ // Assign and return
+ data.inventory = Object.values(inventory);
+ data.forcePowerbook = forcePowerbook;
+ data.techPowerbook = techPowerbook;
+ data.features = Object.values(features);
}
- // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
- const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
- const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
+ /* -------------------------------------------- */
- // Organize Features
- const features = {
- classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
- classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
- archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
- deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true },
- deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true },
- ventures: { label: "SW5E.ItemTypeVenturePl", items: [], hasActions: false, dataset: {type: "venture"}, isVenture: 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"} }
- };
- for ( let f of feats ) {
- if ( f.data.activation.type ) features.active.items.push(f);
- else features.passive.items.push(f);
+ /**
+ * 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");
+ }
}
- classes.sort((a, b) => b.data.levels - a.data.levels);
- features.classes.items = classes;
- features.classfeatures.items = classfeatures;
- features.archetype.items = archetypes;
- features.deployments.items = deployments;
- features.deploymentfeatures.items = deploymentfeatures;
- features.ventures.items = ventures;
- 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);
- data.forcePowerbook = forcePowerbook;
- data.techPowerbook = techPowerbook;
- data.features = Object.values(features);
- }
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers
/* -------------------------------------------- */
- /**
- * 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");
- }
- }
+ /**
+ * Activate event listeners using the prepared sheet HTML
+ * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
+ */
+ activateListeners(html) {
+ super.activateListeners(html);
+ if (!this.isEditable) return;
- /* -------------------------------------------- */
- /* Event Listeners and Handlers
- /* -------------------------------------------- */
+ // Inventory Functions
+ // html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
- /**
- * Activate event listeners using the prepared sheet HTML
- * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
- */
- activateListeners(html) {
- super.activateListeners(html);
- if ( !this.isEditable ) return;
+ // Item State Toggling
+ html.find(".item-toggle").click(this._onToggleItem.bind(this));
- // Inventory Functions
- // html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
+ // Short and Long Rest
+ html.find(".short-rest").click(this._onShortRest.bind(this));
+ html.find(".long-rest").click(this._onLongRest.bind(this));
- // Item State Toggling
- html.find('.item-toggle').click(this._onToggleItem.bind(this));
+ // Rollable sheet actions
+ html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
- // Short and Long Rest
- html.find('.short-rest').click(this._onShortRest.bind(this));
- html.find('.long-rest').click(this._onLongRest.bind(this));
-
- // Rollable sheet actions
- html.find(".rollable[data-action]").click(this._onSheetAction.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 => CONFIG.SW5E.languages[l] || l).join(", ");
- let custom = this.actor.data.data.traits.languages.custom;
- if (custom) langs += ", " + custom.replace(/;/g, ",");
- let content = `
+ // 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) => CONFIG.SW5E.languages[l] || l)
+ .join(", ");
+ let custom = this.actor.data.data.traits.languages.custom;
+ if (custom) langs += ", " + custom.replace(/;/g, ",");
+ let content = `
`;
- // Send to Chat
- let rollBlind = false;
- let rollMode = game.settings.get("core", "rollMode");
- if (rollMode === "blindroll") rollBlind = true;
- let data = {
- user: game.user.data._id,
- content: content,
- blind: rollBlind,
- speaker: {
- actor: this.actor.data._id,
- token: this.actor.token,
- alias: this.actor.name
- },
- type: CONST.CHAT_MESSAGE_TYPES.OTHER
- };
+ // Send to Chat
+ let rollBlind = false;
+ let rollMode = game.settings.get("core", "rollMode");
+ if (rollMode === "blindroll") rollBlind = true;
+ let data = {
+ user: game.user.data._id,
+ content: content,
+ blind: rollBlind,
+ speaker: {
+ actor: this.actor.data._id,
+ token: this.actor.token,
+ alias: this.actor.name
+ },
+ type: CONST.CHAT_MESSAGE_TYPES.OTHER
+ };
- if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM");
- else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)];
+ if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM");
+ else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)];
- ChatMessage.create(data);
- });
+ ChatMessage.create(data);
+ });
- // 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.items.get(itemId);
- new Dialog({
- title: `Deleting ${item.data.name}`,
- content: `Are you sure you want to delete ${item.data.name}?
`,
- buttons: {
- Yes: {
- icon: ' ',
- label: 'Yes',
- callback: dlg => {
- this.actor.deleteOwnedItem(itemId);
- }
- },
- cancel: {
- icon: ' ',
- label: 'No'
- },
- },
- default: 'cancel'
- }).render(true);
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle mouse click events for character sheet actions
- * @param {MouseEvent} event The originating click event
- * @private
- */
- _onSheetAction(event) {
- event.preventDefault();
- const button = event.currentTarget;
- switch( button.dataset.action ) {
- case "rollDeathSave":
- return this.actor.rollDeathSave({event: event});
- case "rollInitiative":
- return this.actor.rollInitiative({createCombatants: true});
+ // 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.items.get(itemId);
+ new Dialog({
+ title: `Deleting ${item.data.name}`,
+ content: `Are you sure you want to delete ${item.data.name}?
`,
+ buttons: {
+ Yes: {
+ icon: ' ',
+ label: "Yes",
+ callback: (dlg) => {
+ this.actor.deleteOwnedItem(itemId);
+ }
+ },
+ cancel: {
+ icon: ' ',
+ label: "No"
+ }
+ },
+ default: "cancel"
+ }).render(true);
+ });
}
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
-
- /**
- * 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.items.get(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();
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async _onDropItemCreate(itemData) {
-
- // Increment the number of class levels of a character instead of creating a new item
- if ( itemData.type === "class" ) {
- const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
- let priorLevel = cls?.data.data.levels ?? 0;
- if ( !!cls ) {
- const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
- if ( next > priorLevel ) {
- itemData.levels = next;
- return cls.update({"data.levels": next});
+ /**
+ * Handle mouse click events for character sheet actions
+ * @param {MouseEvent} event The originating click event
+ * @private
+ */
+ _onSheetAction(event) {
+ event.preventDefault();
+ const button = event.currentTarget;
+ switch (button.dataset.action) {
+ case "rollDeathSave":
+ return this.actor.rollDeathSave({event: event});
+ case "rollInitiative":
+ return this.actor.rollInitiative({createCombatants: true});
}
- }
}
- // Increment the number of deployment ranks of a character instead of creating a new item
- // else if ( itemData.type === "deployment" ) {
- // const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name);
- // let priorRank = rnk?.data.data.ranks ?? 0;
- // if ( !!rnk ) {
- // const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
- // if ( next > priorRank ) {
- // itemData.ranks = next;
- // return rnk.update({"data.ranks": next});
- // }
- // }
- // }
+ /* -------------------------------------------- */
- // Default drop handling if levels were not added
- return super._onDropItemCreate(itemData);
- }
+ /**
+ * 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.items.get(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();
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+ // Increment the number of class levels of a character instead of creating a new item
+ if (itemData.type === "class") {
+ const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
+ let priorLevel = cls?.data.data.levels ?? 0;
+ if (!!cls) {
+ const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
+ if (next > priorLevel) {
+ itemData.levels = next;
+ return cls.update({"data.levels": next});
+ }
+ }
+ }
+
+ // Increment the number of deployment ranks of a character instead of creating a new item
+ // else if ( itemData.type === "deployment" ) {
+ // const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name);
+ // let priorRank = rnk?.data.data.ranks ?? 0;
+ // if ( !!rnk ) {
+ // const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
+ // if ( next > priorRank ) {
+ // itemData.ranks = next;
+ // return rnk.update({"data.ranks": next});
+ // }
+ // }
+ // }
+
+ // Default drop handling if levels were not added
+ 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
- }
- }
+ // 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 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 = $(
+ ` `
+ );
+ favBtn.click((ev) => {
+ app.actor.items.get(item.data._id).update({
+ "flags.favtab.isFavourite": !item.flags.favtab.isFavourite
+ });
+ });
+ html.find(`.item[data-item-id="${item.data._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;
+ let r = !!comps.ritual;
+ 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;
+ }
+ }
}
- let isFav = item.flags.favtab.isFavourite;
+
+ // 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) {
- let favBtn = $(` `);
- favBtn.click(ev => {
- app.actor.items.get(item.data._id).update({
- "flags.favtab.isFavourite": !item.flags.favtab.isFavourite
+ favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev));
+ let handler = (ev) => app._onDragStart(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.items.get(itemId).sheet.render(true);
+ });
+ favtabHtml.find(".item-fav").click((ev) => {
+ let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
+ let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite;
+ app.actor.items.get(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.data._id === dropData.data._id);
+ let siblings = list.filter((i) => i.data._id !== dropData.data._id);
+ let targetId = ev.target.closest(".item").dataset.itemId;
+ let dragTarget = siblings.find((s) => s.data._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.data._id;
+ return update;
+ });
+ app.actor.updateEmbeddedEntity("OwnedItem", updateData);
});
- });
- html.find(`.item[data-item-id="${item.data._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);
- let r = !!(comps.ritual);
- 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._onDragStart(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.items.get(itemId).sheet.render(true);
- });
- favtabHtml.find('.item-fav').click(ev => {
- let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
- let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite
- app.actor.items.get(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.data._id === dropData.data._id);
- let siblings = list.filter(i => i.data._id !== dropData.data._id);
- let targetId = ev.target.closest('.item').dataset.itemId;
- let dragTarget = siblings.find(s => s.data._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.data._id;
- return update;
- });
- app.actor.updateEmbeddedEntity("OwnedItem", updateData);
- });
- }
- tabContainer.append(favtabHtml);
- // if(app.options.editable) {
- // let handler = ev => app._onDragItemStart(ev);
- // tabContainer.find('.item').each((i, li) => {
- // if (li.classList.contains("inventory-header")) return;
- // li.setAttribute("draggable", true);
- // li.addEventListener("dragstart", handler, false);
- // });
- //}
- // 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);
+ tabContainer.append(favtabHtml);
+ // if(app.options.editable) {
+ // let handler = ev => app._onDragItemStart(ev);
+ // tabContainer.find('.item').each((i, li) => {
+ // if (li.classList.contains("inventory-header")) return;
+ // li.setAttribute("draggable", true);
+ // li.addEventListener("dragstart", handler, false);
+ // });
+ //}
+ // 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 => {
- el.active = el.target == target;
- return el;
- })
-
- })
-
+ 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) => {
+ el.active = el.target == target;
+ return el;
+ });
+ });
}
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {
- addFavorites(app, html, data);
- addSubTabs(app, html, data);
-});
\ No newline at end of file
+ addFavorites(app, html, data);
+ addSubTabs(app, html, data);
+});
diff --git a/module/actor/sheets/newSheet/npc.js b/module/actor/sheets/newSheet/npc.js
index 59a94334..699005ea 100644
--- a/module/actor/sheets/newSheet/npc.js
+++ b/module/actor/sheets/newSheet/npc.js
@@ -6,143 +6,154 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPCNew extends ActorSheet5e {
-
- /** @override */
- get template() {
- if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
- return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
- }
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["sw5e", "sheet", "actor", "npc"],
- width: 800,
- tabs: [{
- navSelector: ".root-tabs",
- contentSelector: ".sheet-body",
- initial: "attributes"
- }],
- });
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- static unsupportedItemTypes = new Set(["class"]);
-
- /* -------------------------------------------- */
-
- /**
- * Organize Owned Items for rendering the NPC sheet
- * @private
- */
- _prepareItems(data) {
-
- // Categorize Items as Features and Powers
- const features = {
- weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
- actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
- passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
- equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
- };
-
- // Start by classifying items into groups for rendering
- let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
- item.img = item.img || CONST.DEFAULT_TOKEN;
- item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
- 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));
- if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
- else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
- else arr[2].push(item);
- return arr;
- }, [[], [], []]);
-
- // Apply item filters
- forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
- techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
- other = this._filterItems(other, this._filters.features);
-
- // Organize Powerbook
- const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
- const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
-
- // Organize Features
- for ( let item of other ) {
- if ( item.type === "weapon" ) features.weapons.items.push(item);
- else if ( item.type === "feat" ) {
- if ( item.data.activation.type ) features.actions.items.push(item);
- else features.passive.items.push(item);
- }
- else features.equipment.items.push(item);
+ /** @override */
+ get template() {
+ if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
+ return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
+ }
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "npc"],
+ width: 800,
+ tabs: [
+ {
+ navSelector: ".root-tabs",
+ contentSelector: ".sheet-body",
+ initial: "attributes"
+ }
+ ]
+ });
}
- // Assign and return
- data.features = Object.values(features);
- data.forcePowerbook = forcePowerbook;
- data.techPowerbook = techPowerbook;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /** @override */
+ static unsupportedItemTypes = new Set(["class"]);
- /** @inheritdoc */
- getData(options) {
- const data = super.getData(options);
+ /* -------------------------------------------- */
- // Challenge Rating
- const cr = parseFloat(data.data.details.cr || 0);
- const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
- data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
+ /**
+ * Organize Owned Items for rendering the NPC sheet
+ * @private
+ */
+ _prepareItems(data) {
+ // Categorize Items as Features and Powers
+ const features = {
+ weapons: {
+ label: game.i18n.localize("SW5E.AttackPl"),
+ items: [],
+ hasActions: true,
+ dataset: {"type": "weapon", "weapon-type": "natural"}
+ },
+ actions: {
+ label: game.i18n.localize("SW5E.ActionPl"),
+ items: [],
+ hasActions: true,
+ dataset: {"type": "feat", "activation.type": "action"}
+ },
+ passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
+ equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
+ };
- // Creature Type
- data.labels["type"] = this.actor.labels.creatureType;
- return data;
- }
+ // Start by classifying items into groups for rendering
+ let [forcepowers, techpowers, other] = data.items.reduce(
+ (arr, item) => {
+ item.img = item.img || CONST.DEFAULT_TOKEN;
+ item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
+ 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);
+ if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
+ else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
+ else arr[2].push(item);
+ return arr;
+ },
+ [[], [], []]
+ );
- /* -------------------------------------------- */
- /* Object Updates */
- /* -------------------------------------------- */
+ // Apply item filters
+ forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
+ techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
+ other = this._filterItems(other, this._filters.features);
- /** @override */
- async _updateObject(event, formData) {
+ // Organize Powerbook
+ const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
+ const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
- // Format NPC Challenge Rating
- const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
- let crv = "data.details.cr";
- let cr = formData[crv];
- cr = crs[cr] || parseFloat(cr);
- if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
+ // Organize Features
+ for (let item of other) {
+ if (item.type === "weapon") features.weapons.items.push(item);
+ else if (item.type === "feat") {
+ if (item.data.activation.type) features.actions.items.push(item);
+ else features.passive.items.push(item);
+ } else features.equipment.items.push(item);
+ }
- // Parent ActorSheet update steps
- return super._updateObject(event, formData);
- }
+ // Assign and return
+ data.features = Object.values(features);
+ data.forcePowerbook = forcePowerbook;
+ data.techPowerbook = techPowerbook;
+ }
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
- html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
- }
+ /** @inheritdoc */
+ getData(options) {
+ const data = super.getData(options);
- /* -------------------------------------------- */
+ // Challenge Rating
+ const cr = parseFloat(data.data.details.cr || 0);
+ const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
+ data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
- /**
- * Handle rolling NPC health values using the provided formula
- * @param {Event} event The original click event
- * @private
- */
- _onRollHPFormula(event) {
- event.preventDefault();
- const formula = this.actor.data.data.attributes.hp.formula;
- if ( !formula ) return;
- const hp = new Roll(formula).roll().total;
- AudioHelper.play({src: CONFIG.sounds.dice});
- this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
- }
+ // Creature Type
+ data.labels["type"] = this.actor.labels.creatureType;
+ return data;
+ }
+
+ /* -------------------------------------------- */
+ /* Object Updates */
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _updateObject(event, formData) {
+ // Format NPC Challenge Rating
+ const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
+ let crv = "data.details.cr";
+ let cr = formData[crv];
+ cr = crs[cr] || parseFloat(cr);
+ if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
+
+ // Parent ActorSheet update steps
+ return super._updateObject(event, formData);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling NPC health values using the provided formula
+ * @param {Event} event The original click event
+ * @private
+ */
+ _onRollHPFormula(event) {
+ event.preventDefault();
+ const formula = this.actor.data.data.attributes.hp.formula;
+ if (!formula) return;
+ const hp = new Roll(formula).roll().total;
+ AudioHelper.play({src: CONFIG.sounds.dice});
+ this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
+ }
}
-
diff --git a/module/actor/sheets/newSheet/starship.js b/module/actor/sheets/newSheet/starship.js
index e27354d6..bf7e397e 100644
--- a/module/actor/sheets/newSheet/starship.js
+++ b/module/actor/sheets/newSheet/starship.js
@@ -6,150 +6,164 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eStarship extends ActorSheet5e {
-
- /** @override */
- get template() {
- if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
- return `systems/sw5e/templates/actors/newActor/starship.html`;
- }
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["sw5e", "sheet", "actor", "starship"],
- width: 800,
- tabs: [{
- navSelector: ".root-tabs",
- contentSelector: ".sheet-body",
- initial: "attributes"
- }],
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Organize Owned Items for rendering the starship sheet
- * @private
- */
- _prepareItems(data) {
-
- // Categorize Items as Features and Powers
- const features = {
- weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
- passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
- equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
- starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} },
- starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} }
- };
-
- // Start by classifying items into groups for rendering
- let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
- item.img = item.img || CONST.DEFAULT_TOKEN;
- item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
- 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));
- if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
- else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
- else arr[2].push(item);
- return arr;
- }, [[], [], []]);
-
- // Apply item filters
- forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
- techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
- other = this._filterItems(other, this._filters.features);
-
- // Organize Powerbook
-// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
-// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
-
- // Organize Features
- for ( let item of other ) {
- if ( item.type === "weapon" ) features.weapons.items.push(item);
- else if ( item.type === "feat" ) {
- if ( item.data.activation.type ) features.actions.items.push(item);
- else features.passive.items.push(item);
- }
- else if ( item.type === "starshipfeature" ) {
- features.starshipfeatures.items.push(item);
- }
- else if ( item.type === "starshipmod" ) {
- features.starshipmods.items.push(item);
- }
- else features.equipment.items.push(item);
+ /** @override */
+ get template() {
+ if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
+ return `systems/sw5e/templates/actors/newActor/starship.html`;
+ }
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "starship"],
+ width: 800,
+ tabs: [
+ {
+ navSelector: ".root-tabs",
+ contentSelector: ".sheet-body",
+ initial: "attributes"
+ }
+ ]
+ });
}
- // Assign and return
- data.features = Object.values(features);
-// data.forcePowerbook = forcePowerbook;
-// data.techPowerbook = techPowerbook;
- }
+ /* -------------------------------------------- */
+ /**
+ * Organize Owned Items for rendering the starship sheet
+ * @private
+ */
+ _prepareItems(data) {
+ // Categorize Items as Features and Powers
+ const features = {
+ weapons: {
+ label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
+ items: [],
+ hasActions: true,
+ dataset: {"type": "weapon", "weapon-type": "natural"}
+ },
+ passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
+ equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
+ starshipfeatures: {
+ label: game.i18n.localize("SW5E.StarshipfeaturePl"),
+ items: [],
+ hasActions: true,
+ dataset: {type: "starshipfeature"}
+ },
+ starshipmods: {
+ label: game.i18n.localize("SW5E.StarshipmodPl"),
+ items: [],
+ hasActions: false,
+ dataset: {type: "starshipmod"}
+ }
+ };
- /* -------------------------------------------- */
+ // Start by classifying items into groups for rendering
+ let [forcepowers, techpowers, other] = data.items.reduce(
+ (arr, item) => {
+ item.img = item.img || CONST.DEFAULT_TOKEN;
+ item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
+ 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);
+ if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
+ else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
+ else arr[2].push(item);
+ return arr;
+ },
+ [[], [], []]
+ );
- /** @override */
- getData(options) {
- const data = super.getData(options);
+ // Apply item filters
+ forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
+ techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
+ other = this._filterItems(other, this._filters.features);
- // Add Size info
- data.isTiny = data.actor.data.traits.size === "tiny";
- data.isSmall = data.actor.data.traits.size === "sm";
- data.isMedium = data.actor.data.traits.size === "med";
- data.isLarge = data.actor.data.traits.size === "lg";
- data.isHuge = data.actor.data.traits.size === "huge";
- data.isGargantuan = data.actor.data.traits.size === "grg";
+ // Organize Powerbook
+ // const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
+ // const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
- // Challenge Rating
- const cr = parseFloat(data.data.details.cr || 0);
- const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
- data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
- return data;
- }
+ // Organize Features
+ for (let item of other) {
+ if (item.type === "weapon") features.weapons.items.push(item);
+ else if (item.type === "feat") {
+ if (item.data.activation.type) features.actions.items.push(item);
+ else features.passive.items.push(item);
+ } else if (item.type === "starshipfeature") {
+ features.starshipfeatures.items.push(item);
+ } else if (item.type === "starshipmod") {
+ features.starshipmods.items.push(item);
+ } else features.equipment.items.push(item);
+ }
- /* -------------------------------------------- */
- /* Object Updates */
- /* -------------------------------------------- */
+ // Assign and return
+ data.features = Object.values(features);
+ // data.forcePowerbook = forcePowerbook;
+ // data.techPowerbook = techPowerbook;
+ }
- /** @override */
- async _updateObject(event, formData) {
+ /* -------------------------------------------- */
- // Format NPC Challenge Rating
- const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
- let crv = "data.details.cr";
- let cr = formData[crv];
- cr = crs[cr] || parseFloat(cr);
- if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
+ /** @override */
+ getData(options) {
+ const data = super.getData(options);
- // Parent ActorSheet update steps
- return super._updateObject(event, formData);
- }
+ // Add Size info
+ data.isTiny = data.actor.data.traits.size === "tiny";
+ data.isSmall = data.actor.data.traits.size === "sm";
+ data.isMedium = data.actor.data.traits.size === "med";
+ data.isLarge = data.actor.data.traits.size === "lg";
+ data.isHuge = data.actor.data.traits.size === "huge";
+ data.isGargantuan = data.actor.data.traits.size === "grg";
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
+ // Challenge Rating
+ const cr = parseFloat(data.data.details.cr || 0);
+ const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
+ data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
+ return data;
+ }
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
- html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
- }
+ /* -------------------------------------------- */
+ /* Object Updates */
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /** @override */
+ async _updateObject(event, formData) {
+ // Format NPC Challenge Rating
+ const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
+ let crv = "data.details.cr";
+ let cr = formData[crv];
+ cr = crs[cr] || parseFloat(cr);
+ if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
- /**
- * Handle rolling NPC health values using the provided formula
- * @param {Event} event The original click event
- * @private
- */
- _onRollHPFormula(event) {
- event.preventDefault();
- const formula = this.actor.data.data.attributes.hp.formula;
- if ( !formula ) return;
- const hp = new Roll(formula).roll().total;
- AudioHelper.play({src: CONFIG.sounds.dice});
- this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
- }
+ // Parent ActorSheet update steps
+ return super._updateObject(event, formData);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling NPC health values using the provided formula
+ * @param {Event} event The original click event
+ * @private
+ */
+ _onRollHPFormula(event) {
+ event.preventDefault();
+ const formula = this.actor.data.data.attributes.hp.formula;
+ if (!formula) return;
+ const hp = new Roll(formula).roll().total;
+ AudioHelper.play({src: CONFIG.sounds.dice});
+ this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
+ }
}
diff --git a/module/actor/sheets/newSheet/vehicle.js b/module/actor/sheets/newSheet/vehicle.js
index b5e28a1c..a5c6e2ea 100644
--- a/module/actor/sheets/newSheet/vehicle.js
+++ b/module/actor/sheets/newSheet/vehicle.js
@@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js";
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
- /**
- * Define default rendering options for the Vehicle sheet.
- * @returns {Object}
- */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["sw5e", "sheet", "actor", "vehicle"],
- width: 605,
- height: 680
- });
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- static unsupportedItemTypes = new Set(["class"]);
-
- /* -------------------------------------------- */
-
-
- /**
- * Creates a new cargo entry for a vehicle Actor.
- */
- static get newCargo() {
- return {
- name: '',
- quantity: 1
- };
- }
-
- /* -------------------------------------------- */
-
- /**
- * Compute the total weight of the vehicle's cargo.
- * @param {Number} totalWeight The cumulative item weight from inventory items
- * @param {Object} actorData The data object for the Actor being rendered
- * @returns {{max: number, value: number, pct: number}}
- * @private
- */
- _computeEncumbrance(totalWeight, actorData) {
-
- // Compute currency weight
- const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
- totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
-
- // Vehicle weights are an order of magnitude greater.
- totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
-
- // Compute overall encumbrance
- const max = actorData.data.attributes.capacity.cargo;
- const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
- return {value: totalWeight.toNearest(0.1), max, pct};
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- _getMovementSpeed(actorData, largestPrimary=true) {
- return super._getMovementSpeed(actorData, largestPrimary);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare items that are mounted to a vehicle and require one or more crew
- * to operate.
- * @private
- */
- _prepareCrewedItem(item) {
-
- // Determine crewed status
- const isCrewed = item.data.crewed;
- item.toggleClass = isCrewed ? 'active' : '';
- item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
-
- // Handle crew actions
- if (item.type === 'feat' && item.data.activation.type === 'crew') {
- item.crew = item.data.activation.cost;
- item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
- if (item.data.cover === .5) item.cover = '½';
- else if (item.data.cover === .75) item.cover = '¾';
- else if (item.data.cover === null) item.cover = '—';
- if (item.crew < 1 || item.crew === null) item.crew = '—';
+ /**
+ * Define default rendering options for the Vehicle sheet.
+ * @returns {Object}
+ */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "vehicle"],
+ width: 605,
+ height: 680
+ });
}
- // Prepare vehicle weapons
- if (item.type === 'equipment' || item.type === 'weapon') {
- item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
- }
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /** @override */
+ static unsupportedItemTypes = new Set(["class"]);
- /**
- * Organize Owned Items for rendering the Vehicle sheet.
- * @private
- */
- _prepareItems(data) {
- const cargoColumns = [{
- label: game.i18n.localize('SW5E.Quantity'),
- css: 'item-qty',
- property: 'quantity',
- editable: 'Number'
- }];
+ /* -------------------------------------------- */
- const equipmentColumns = [{
- label: game.i18n.localize('SW5E.Quantity'),
- css: 'item-qty',
- property: 'data.quantity'
- }, {
- label: game.i18n.localize('SW5E.AC'),
- css: 'item-ac',
- property: 'data.armor.value'
- }, {
- label: game.i18n.localize('SW5E.HP'),
- css: 'item-hp',
- property: 'data.hp.value',
- editable: 'Number'
- }, {
- label: game.i18n.localize('SW5E.Threshold'),
- css: 'item-threshold',
- property: 'threshold'
- }];
-
- const features = {
- actions: {
- label: game.i18n.localize('SW5E.ActionPl'),
- items: [],
- crewable: true,
- dataset: {type: 'feat', 'activation.type': 'crew'},
- columns: [{
- label: game.i18n.localize('SW5E.VehicleCrew'),
- css: 'item-crew',
- property: 'crew'
- }, {
- label: game.i18n.localize('SW5E.Cover'),
- css: 'item-cover',
- property: 'cover'
- }]
- },
- equipment: {
- label: game.i18n.localize('SW5E.ItemTypeEquipment'),
- items: [],
- crewable: true,
- dataset: {type: 'equipment', 'armor.type': 'vehicle'},
- columns: equipmentColumns
- },
- passive: {
- label: game.i18n.localize('SW5E.Features'),
- items: [],
- dataset: {type: 'feat'}
- },
- reactions: {
- label: game.i18n.localize('SW5E.ReactionPl'),
- items: [],
- dataset: {type: 'feat', 'activation.type': 'reaction'}
- },
- weapons: {
- label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
- items: [],
- crewable: true,
- dataset: {type: 'weapon', 'weapon-type': 'siege'},
- columns: equipmentColumns
- }
- };
-
- const cargo = {
- crew: {
- label: game.i18n.localize('SW5E.VehicleCrew'),
- items: data.data.cargo.crew,
- css: 'cargo-row crew',
- editableName: true,
- dataset: {type: 'crew'},
- columns: cargoColumns
- },
- passengers: {
- label: game.i18n.localize('SW5E.VehiclePassengers'),
- items: data.data.cargo.passengers,
- css: 'cargo-row passengers',
- editableName: true,
- dataset: {type: 'passengers'},
- columns: cargoColumns
- },
- cargo: {
- label: game.i18n.localize('SW5E.VehicleCargo'),
- items: [],
- dataset: {type: 'loot'},
- columns: [{
- label: game.i18n.localize('SW5E.Quantity'),
- css: 'item-qty',
- property: 'data.quantity',
- editable: 'Number'
- }, {
- label: game.i18n.localize('SW5E.Price'),
- css: 'item-price',
- property: 'data.price',
- editable: 'Number'
- }, {
- label: game.i18n.localize('SW5E.Weight'),
- css: 'item-weight',
- property: 'data.weight',
- editable: 'Number'
- }]
- }
- };
-
- // Classify items owned by the vehicle and compute total cargo weight
- let totalWeight = 0;
- for (const item of data.items) {
- this._prepareCrewedItem(item);
-
- // Handle cargo explicitly
- const isCargo = item.flags.sw5e?.vehicleCargo === true;
- if ( isCargo ) {
- totalWeight += (item.data.weight || 0) * item.data.quantity;
- cargo.cargo.items.push(item);
- continue;
- }
-
- // Handle non-cargo item types
- switch ( item.type ) {
- case "weapon":
- features.weapons.items.push(item);
- break;
- case "equipment":
- features.equipment.items.push(item);
- break;
- case "feat":
- if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item);
- else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
- else features.actions.items.push(item);
- break;
- default:
- totalWeight += (item.data.weight || 0) * item.data.quantity;
- cargo.cargo.items.push(item);
- }
+ /**
+ * Creates a new cargo entry for a vehicle Actor.
+ */
+ static get newCargo() {
+ return {
+ name: "",
+ quantity: 1
+ };
}
- // Update the rendering context data
- data.features = Object.values(features);
- data.cargo = Object.values(cargo);
- data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
+ /**
+ * Compute the total weight of the vehicle's cargo.
+ * @param {Number} totalWeight The cumulative item weight from inventory items
+ * @param {Object} actorData The data object for the Actor being rendered
+ * @returns {{max: number, value: number, pct: number}}
+ * @private
+ */
+ _computeEncumbrance(totalWeight, actorData) {
+ // Compute currency weight
+ const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
+ totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
- if (!this.isEditable) return;
+ // Vehicle weights are an order of magnitude greater.
+ totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
- html.find('.item-toggle').click(this._onToggleItem.bind(this));
- html.find('.item-hp input')
- .click(evt => evt.target.select())
- .change(this._onHPChange.bind(this));
-
- html.find('.item:not(.cargo-row) input[data-property]')
- .click(evt => evt.target.select())
- .change(this._onEditInSheet.bind(this));
-
- html.find('.cargo-row input')
- .click(evt => evt.target.select())
- .change(this._onCargoRowChange.bind(this));
-
- if (this.actor.data.data.attributes.actions.stations) {
- html.find('.counter.actions, .counter.action-thresholds').hide();
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
- * @param event {Event}
- * @returns {Promise|null}
- * @private
- */
- _onCargoRowChange(event) {
- event.preventDefault();
- const target = event.currentTarget;
- const row = target.closest('.item');
- const idx = Number(row.dataset.itemId);
- const property = row.classList.contains('crew') ? 'crew' : 'passengers';
-
- // Get the cargo entry
- const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
- const entry = cargo[idx];
- if (!entry) return null;
-
- // Update the cargo value
- const key = target.dataset.property || 'name';
- const type = target.dataset.dtype;
- let value = target.value;
- if (type === 'Number') value = Number(value);
- entry[key] = value;
-
- // Perform the Actor update
- return this.actor.update({[`data.cargo.${property}`]: cargo});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle editing certain values like quantity, price, and weight in-sheet.
- * @param event {Event}
- * @returns {Promise- }
- * @private
- */
- _onEditInSheet(event) {
- event.preventDefault();
- const itemID = event.currentTarget.closest('.item').dataset.itemId;
- const item = this.actor.items.get(itemID);
- const property = event.currentTarget.dataset.property;
- const type = event.currentTarget.dataset.dtype;
- let value = event.currentTarget.value;
- switch (type) {
- case 'Number': value = parseInt(value); break;
- case 'Boolean': value = value === 'true'; break;
- }
- return item.update({[`${property}`]: value});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle creating a new crew or passenger row.
- * @param event {Event}
- * @returns {Promise
}
- * @private
- */
- _onItemCreate(event) {
- event.preventDefault();
- const target = event.currentTarget;
- const type = target.dataset.type;
- if (type === 'crew' || type === 'passengers') {
- const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
- cargo.push(this.constructor.newCargo);
- return this.actor.update({[`data.cargo.${type}`]: cargo});
- }
- return super._onItemCreate(event);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle deleting a crew or passenger row.
- * @param event {Event}
- * @returns {Promise}
- * @private
- */
- _onItemDelete(event) {
- event.preventDefault();
- const row = event.currentTarget.closest('.item');
- if (row.classList.contains('cargo-row')) {
- const idx = Number(row.dataset.itemId);
- const type = row.classList.contains('crew') ? 'crew' : 'passengers';
- const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
- return this.actor.update({[`data.cargo.${type}`]: cargo});
+ // Compute overall encumbrance
+ const max = actorData.data.attributes.capacity.cargo;
+ const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
+ return {value: totalWeight.toNearest(0.1), max, pct};
}
- return super._onItemDelete(event);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /** @override */
+ _getMovementSpeed(actorData, largestPrimary = true) {
+ return super._getMovementSpeed(actorData, largestPrimary);
+ }
- /** @override */
- async _onDropItemCreate(itemData) {
- const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
- const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
- foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
- return super._onDropItemCreate(itemData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Prepare items that are mounted to a vehicle and require one or more crew
+ * to operate.
+ * @private
+ */
+ _prepareCrewedItem(item) {
+ // Determine crewed status
+ const isCrewed = item.data.crewed;
+ item.toggleClass = isCrewed ? "active" : "";
+ item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
- /**
- * Special handling for editing HP to clamp it within appropriate range.
- * @param event {Event}
- * @returns {Promise- }
- * @private
- */
- _onHPChange(event) {
- event.preventDefault();
- const itemID = event.currentTarget.closest('.item').dataset.itemId;
- const item = this.actor.items.get(itemID);
- const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
- event.currentTarget.value = hp;
- return item.update({'data.hp.value': hp});
- }
+ // Handle crew actions
+ if (item.type === "feat" && item.data.activation.type === "crew") {
+ item.crew = item.data.activation.cost;
+ item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
+ if (item.data.cover === 0.5) item.cover = "½";
+ else if (item.data.cover === 0.75) item.cover = "¾";
+ else if (item.data.cover === null) item.cover = "—";
+ if (item.crew < 1 || item.crew === null) item.crew = "—";
+ }
- /* -------------------------------------------- */
+ // Prepare vehicle weapons
+ if (item.type === "equipment" || item.type === "weapon") {
+ item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
+ }
+ }
- /**
- * Handle toggling an item's crewed status.
- * @param event {Event}
- * @returns {Promise
- }
- * @private
- */
- _onToggleItem(event) {
- event.preventDefault();
- const itemID = event.currentTarget.closest('.item').dataset.itemId;
- const item = this.actor.items.get(itemID);
- const crewed = !!item.data.data.crewed;
- return item.update({'data.crewed': !crewed});
- }
-};
+ /* -------------------------------------------- */
+
+ /**
+ * Organize Owned Items for rendering the Vehicle sheet.
+ * @private
+ */
+ _prepareItems(data) {
+ const cargoColumns = [
+ {
+ label: game.i18n.localize("SW5E.Quantity"),
+ css: "item-qty",
+ property: "quantity",
+ editable: "Number"
+ }
+ ];
+
+ const equipmentColumns = [
+ {
+ label: game.i18n.localize("SW5E.Quantity"),
+ css: "item-qty",
+ property: "data.quantity"
+ },
+ {
+ label: game.i18n.localize("SW5E.AC"),
+ css: "item-ac",
+ property: "data.armor.value"
+ },
+ {
+ label: game.i18n.localize("SW5E.HP"),
+ css: "item-hp",
+ property: "data.hp.value",
+ editable: "Number"
+ },
+ {
+ label: game.i18n.localize("SW5E.Threshold"),
+ css: "item-threshold",
+ property: "threshold"
+ }
+ ];
+
+ const features = {
+ actions: {
+ label: game.i18n.localize("SW5E.ActionPl"),
+ items: [],
+ crewable: true,
+ dataset: {"type": "feat", "activation.type": "crew"},
+ columns: [
+ {
+ label: game.i18n.localize("SW5E.VehicleCrew"),
+ css: "item-crew",
+ property: "crew"
+ },
+ {
+ label: game.i18n.localize("SW5E.Cover"),
+ css: "item-cover",
+ property: "cover"
+ }
+ ]
+ },
+ equipment: {
+ label: game.i18n.localize("SW5E.ItemTypeEquipment"),
+ items: [],
+ crewable: true,
+ dataset: {"type": "equipment", "armor.type": "vehicle"},
+ columns: equipmentColumns
+ },
+ passive: {
+ label: game.i18n.localize("SW5E.Features"),
+ items: [],
+ dataset: {type: "feat"}
+ },
+ reactions: {
+ label: game.i18n.localize("SW5E.ReactionPl"),
+ items: [],
+ dataset: {"type": "feat", "activation.type": "reaction"}
+ },
+ weapons: {
+ label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
+ items: [],
+ crewable: true,
+ dataset: {"type": "weapon", "weapon-type": "siege"},
+ columns: equipmentColumns
+ }
+ };
+
+ const cargo = {
+ crew: {
+ label: game.i18n.localize("SW5E.VehicleCrew"),
+ items: data.data.cargo.crew,
+ css: "cargo-row crew",
+ editableName: true,
+ dataset: {type: "crew"},
+ columns: cargoColumns
+ },
+ passengers: {
+ label: game.i18n.localize("SW5E.VehiclePassengers"),
+ items: data.data.cargo.passengers,
+ css: "cargo-row passengers",
+ editableName: true,
+ dataset: {type: "passengers"},
+ columns: cargoColumns
+ },
+ cargo: {
+ label: game.i18n.localize("SW5E.VehicleCargo"),
+ items: [],
+ dataset: {type: "loot"},
+ columns: [
+ {
+ label: game.i18n.localize("SW5E.Quantity"),
+ css: "item-qty",
+ property: "data.quantity",
+ editable: "Number"
+ },
+ {
+ label: game.i18n.localize("SW5E.Price"),
+ css: "item-price",
+ property: "data.price",
+ editable: "Number"
+ },
+ {
+ label: game.i18n.localize("SW5E.Weight"),
+ css: "item-weight",
+ property: "data.weight",
+ editable: "Number"
+ }
+ ]
+ }
+ };
+
+ // Classify items owned by the vehicle and compute total cargo weight
+ let totalWeight = 0;
+ for (const item of data.items) {
+ this._prepareCrewedItem(item);
+
+ // Handle cargo explicitly
+ const isCargo = item.flags.sw5e?.vehicleCargo === true;
+ if (isCargo) {
+ totalWeight += (item.data.weight || 0) * item.data.quantity;
+ cargo.cargo.items.push(item);
+ continue;
+ }
+
+ // Handle non-cargo item types
+ switch (item.type) {
+ case "weapon":
+ features.weapons.items.push(item);
+ break;
+ case "equipment":
+ features.equipment.items.push(item);
+ break;
+ case "feat":
+ if (!item.data.activation.type || item.data.activation.type === "none")
+ features.passive.items.push(item);
+ else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
+ else features.actions.items.push(item);
+ break;
+ default:
+ totalWeight += (item.data.weight || 0) * item.data.quantity;
+ cargo.cargo.items.push(item);
+ }
+ }
+
+ // Update the rendering context data
+ data.features = Object.values(features);
+ data.cargo = Object.values(cargo);
+ data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ if (!this.isEditable) return;
+
+ html.find(".item-toggle").click(this._onToggleItem.bind(this));
+ html.find(".item-hp input")
+ .click((evt) => evt.target.select())
+ .change(this._onHPChange.bind(this));
+
+ html.find(".item:not(.cargo-row) input[data-property]")
+ .click((evt) => evt.target.select())
+ .change(this._onEditInSheet.bind(this));
+
+ html.find(".cargo-row input")
+ .click((evt) => evt.target.select())
+ .change(this._onCargoRowChange.bind(this));
+
+ if (this.actor.data.data.attributes.actions.stations) {
+ html.find(".counter.actions, .counter.action-thresholds").hide();
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
+ * @param event {Event}
+ * @returns {Promise
|null}
+ * @private
+ */
+ _onCargoRowChange(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ const row = target.closest(".item");
+ const idx = Number(row.dataset.itemId);
+ const property = row.classList.contains("crew") ? "crew" : "passengers";
+
+ // Get the cargo entry
+ const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
+ const entry = cargo[idx];
+ if (!entry) return null;
+
+ // Update the cargo value
+ const key = target.dataset.property || "name";
+ const type = target.dataset.dtype;
+ let value = target.value;
+ if (type === "Number") value = Number(value);
+ entry[key] = value;
+
+ // Perform the Actor update
+ return this.actor.update({[`data.cargo.${property}`]: cargo});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle editing certain values like quantity, price, and weight in-sheet.
+ * @param event {Event}
+ * @returns {Promise- }
+ * @private
+ */
+ _onEditInSheet(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const property = event.currentTarget.dataset.property;
+ const type = event.currentTarget.dataset.dtype;
+ let value = event.currentTarget.value;
+ switch (type) {
+ case "Number":
+ value = parseInt(value);
+ break;
+ case "Boolean":
+ value = value === "true";
+ break;
+ }
+ return item.update({[`${property}`]: value});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle creating a new crew or passenger row.
+ * @param event {Event}
+ * @returns {Promise
}
+ * @private
+ */
+ _onItemCreate(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ const type = target.dataset.type;
+ if (type === "crew" || type === "passengers") {
+ const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
+ cargo.push(this.constructor.newCargo);
+ return this.actor.update({[`data.cargo.${type}`]: cargo});
+ }
+ return super._onItemCreate(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle deleting a crew or passenger row.
+ * @param event {Event}
+ * @returns {Promise}
+ * @private
+ */
+ _onItemDelete(event) {
+ event.preventDefault();
+ const row = event.currentTarget.closest(".item");
+ if (row.classList.contains("cargo-row")) {
+ const idx = Number(row.dataset.itemId);
+ const type = row.classList.contains("crew") ? "crew" : "passengers";
+ const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
+ return this.actor.update({[`data.cargo.${type}`]: cargo});
+ }
+
+ return super._onItemDelete(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+ const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
+ const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
+ foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
+ return super._onDropItemCreate(itemData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Special handling for editing HP to clamp it within appropriate range.
+ * @param event {Event}
+ * @returns {Promise- }
+ * @private
+ */
+ _onHPChange(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
+ event.currentTarget.value = hp;
+ return item.update({"data.hp.value": hp});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling an item's crewed status.
+ * @param event {Event}
+ * @returns {Promise
- }
+ * @private
+ */
+ _onToggleItem(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const crewed = !!item.data.data.crewed;
+ return item.update({"data.crewed": !crewed});
+ }
+}
diff --git a/module/actor/sheets/oldSheets/base.js b/module/actor/sheets/oldSheets/base.js
index f0a7a83b..acab8b2a 100644
--- a/module/actor/sheets/oldSheets/base.js
+++ b/module/actor/sheets/oldSheets/base.js
@@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
-import {SW5E} from '../../../config.js';
+import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
@@ -14,902 +14,907 @@ import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effe
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
- constructor(...args) {
- super(...args);
+ constructor(...args) {
+ super(...args);
+
+ /**
+ * Track the set of item filters which are applied
+ * @type {Set}
+ */
+ this._filters = {
+ inventory: new Set(),
+ powerbook: new Set(),
+ features: new Set(),
+ effects: new Set()
+ };
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ scrollY: [
+ ".inventory .inventory-list",
+ ".features .inventory-list",
+ ".powerbook .inventory-list",
+ ".effects .inventory-list"
+ ],
+ tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
+ });
+ }
+
+ /* -------------------------------------------- */
/**
- * Track the set of item filters which are applied
- * @type {Set}
+ * A set of item types that should be prevented from being dropped on this type of actor sheet.
+ * @type {Set
}
*/
- this._filters = {
- inventory: new Set(),
- powerbook: new Set(),
- features: new Set(),
- effects: new Set()
- };
- }
+ static unsupportedItemTypes = new Set();
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- scrollY: [
- ".inventory .inventory-list",
- ".features .inventory-list",
- ".powerbook .inventory-list",
- ".effects .inventory-list"
- ],
- tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * A set of item types that should be prevented from being dropped on this type of actor sheet.
- * @type {Set}
- */
- static unsupportedItemTypes = new Set();
-
- /* -------------------------------------------- */
-
-
- /** @override */
- get template() {
- 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`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData(options) {
-
- // Basic data
- let isOwner = this.actor.isOwner;
- const data = {
- owner: isOwner,
- limited: this.actor.limited,
- options: this.options,
- editable: this.isEditable,
- cssClass: isOwner ? "editable" : "locked",
- isCharacter: this.actor.type === "character",
- isNPC: this.actor.type === "npc",
- isStarship: this.actor.type === "starship",
- isVehicle: this.actor.type === 'vehicle',
- config: CONFIG.SW5E,
- rollData: this.actor.getRollData.bind(this.actor)
- };
-
- // The Actor's data
- const actorData = this.actor.data.toObject(false);
- data.actor = actorData;
- data.data = actorData.data;
-
- // Owned Items
- data.items = actorData.items;
- for ( let i of data.items ) {
- const item = this.actor.items.get(i._id);
- i.labels = item.labels;
- }
- data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
-
- // Labels and filters
- data.labels = this.actor.labels || {};
- data.filters = this._filters;
-
- // Ability Scores
- for ( let [a, abl] of Object.entries(actorData.data.abilities)) {
- abl.icon = this._getProficiencyIcon(abl.proficient);
- abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
- abl.label = CONFIG.SW5E.abilities[a];
+ /** @override */
+ get template() {
+ 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`;
}
- // Skills
- if (actorData.data.skills) {
- for ( let [s, skl] of Object.entries(actorData.data.skills)) {
- skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
- skl.icon = this._getProficiencyIcon(skl.value);
- skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
- skl.label = CONFIG.SW5E.skills[s];
- }
- }
+ /* -------------------------------------------- */
- // Movement speeds
- data.movement = this._getMovementSpeed(actorData);
+ /** @override */
+ getData(options) {
+ // Basic data
+ let isOwner = this.actor.isOwner;
+ const data = {
+ owner: isOwner,
+ limited: this.actor.limited,
+ options: this.options,
+ editable: this.isEditable,
+ cssClass: isOwner ? "editable" : "locked",
+ isCharacter: this.actor.type === "character",
+ isNPC: this.actor.type === "npc",
+ isStarship: this.actor.type === "starship",
+ isVehicle: this.actor.type === "vehicle",
+ config: CONFIG.SW5E,
+ rollData: this.actor.getRollData.bind(this.actor)
+ };
- // Senses
- data.senses = this._getSenses(actorData);
+ // The Actor's data
+ const actorData = this.actor.data.toObject(false);
+ data.actor = actorData;
+ data.data = actorData.data;
- // Update traits
- this._prepareTraits(actorData.data.traits);
-
- // Prepare owned items
- this._prepareItems(data);
-
- // Prepare active effects
- data.effects = prepareActiveEffectCategories(this.actor.effects);
-
- // Return data to the sheet
- return data
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare the display of movement speed data for the Actor*
- * @param {object} actorData The Actor data being prepared.
- * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
- * @returns {{primary: string, special: string}}
- * @private
- */
- _getMovementSpeed(actorData, largestPrimary=false) {
- const movement = actorData.data.attributes.movement || {};
-
- // Prepare an array of available movement speeds
- let 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}`]
- ]
- if ( largestPrimary ) {
- speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
- }
-
- // Filter and sort speeds on their values
- speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
-
- // Case 1: Largest as primary
- if ( largestPrimary ) {
- let primary = speeds.shift();
- return {
- primary: `${primary ? primary[1] : "0"} ${movement.units}`,
- special: speeds.map(s => s[1]).join(", ")
- }
- }
-
- // Case 2: Walk as primary
- else {
- return {
- primary: `${movement.walk || 0} ${movement.units}`,
- special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
- }
- }
- }
-
- /* -------------------------------------------- */
-
- _getSenses(actorData) {
- const senses = actorData.data.attributes.senses || {};
- const tags = {};
- for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
- const v = senses[k] ?? 0
- if ( v === 0 ) continue;
- tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
- }
- if ( !!senses.special ) tags["special"] = senses.special;
- return tags;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
- * @param {object} traits The raw traits data object from the actor data
- * @private
- */
- _prepareTraits(traits) {
- const map = {
- "dr": CONFIG.SW5E.damageResistanceTypes,
- "di": CONFIG.SW5E.damageResistanceTypes,
- "dv": CONFIG.SW5E.damageResistanceTypes,
- "ci": CONFIG.SW5E.conditionTypes,
- "languages": CONFIG.SW5E.languages,
- "armorProf": CONFIG.SW5E.armorProficiencies,
- "weaponProf": CONFIG.SW5E.weaponProficiencies,
- "toolProf": CONFIG.SW5E.toolProficiencies
- };
- for ( let [t, choices] of Object.entries(map) ) {
- const trait = traits[t];
- if ( !trait ) continue;
- let values = [];
- if ( trait.value ) {
- values = trait.value instanceof Array ? trait.value : [trait.value];
- }
- trait.selected = values.reduce((obj, t) => {
- obj[t] = choices[t];
- return obj;
- }, {});
-
- // Add custom entry
- if ( trait.custom ) {
- trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
- }
- trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Insert a power into the powerbook object when rendering the character sheet
- * @param {Object} data The Actor data being prepared
- * @param {Array} powers The power data being prepared
- * @private
- */
- _preparePowerbook(data, powers) {
- const owner = this.actor.isOwner;
- const levels = data.data.powers;
- const powerbook = {};
-
- // Define some mappings
- const sections = {
- "atwill": -20,
- "innate": -10,
- "pact": 0.5
- };
-
- // Label power slot uses headers
- const useLabels = {
- "-20": "-",
- "-10": "-",
- "0": "∞"
- };
-
- // Format a powerbook entry for a certain indexed level
- const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
- powerbook[i] = {
- order: i,
- label: label,
- usesSlots: i > 0,
- canCreate: owner,
- canPrepare: (data.actor.type === "character") && (i >= 1),
- powers: [],
- 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
- };
- };
-
- // Determine the maximum power level which has a slot
- const maxLevel = Array.fromRange(10).reduce((max, i) => {
- if ( i === 0 ) return max;
- const level = levels[`power${i}`];
- if ( (level.max || level.override ) && ( i > max ) ) max = i;
- return max;
- }, 0);
-
- // 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++) {
- const sl = `power${lvl}`;
- registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
- }
- }
-
- // Pact magic users have cantrips and a pact magic section
- // TODO: Check if this is needed, we've removed pacts everywhere else
- if ( levels.pact && levels.pact.max ) {
- if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
- const l = levels.pact;
- const config = CONFIG.SW5E.powerPreparationModes.pact;
- const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
- const label = `${config} — ${level}`;
- registerSection("pact", sections.pact, label, {
- prepMode: "pact",
- value: l.value,
- max: l.max,
- override: l.override
- });
- }
-
- // Iterate over every power item, adding powers to the powerbook by section
- powers.forEach(power => {
- const mode = power.data.preparation.mode || "prepared";
- let s = power.data.level || 0;
- const sl = `power${s}`;
-
- // Specialized powercasting modes (if they exist)
- if ( mode in sections ) {
- s = sections[mode];
- if ( !powerbook[s] ){
- 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
- });
+ // Owned Items
+ data.items = actorData.items;
+ for (let i of data.items) {
+ const item = this.actor.items.get(i._id);
+ i.labels = item.labels;
}
- }
+ data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
- // 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: levels[sl]});
- }
+ // Labels and filters
+ data.labels = this.actor.labels || {};
+ data.filters = this._filters;
- // Add the power to the relevant heading
- powerbook[s].powers.push(power);
- });
-
- // Sort the powerbook by section level
- const sorted = Object.values(powerbook);
- sorted.sort((a, b) => a.order - b.order);
- return sorted;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Determine whether an Owned Item will be shown based on the current set of filters
- * @return {boolean}
- * @private
- */
- _filterItems(items, filters) {
- return items.filter(item => {
- const data = item.data;
-
- // Action usage
- for ( let f of ["action", "bonus", "reaction"] ) {
- if ( filters.has(f) ) {
- if ((data.activation && (data.activation.type !== f))) return false;
+ // Ability Scores
+ for (let [a, abl] of Object.entries(actorData.data.abilities)) {
+ abl.icon = this._getProficiencyIcon(abl.proficient);
+ abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
+ abl.label = CONFIG.SW5E.abilities[a];
}
- }
- // Power-specific filters
- if ( filters.has("ritual") ) {
- if (data.components.ritual !== true) return false;
- }
- if ( filters.has("concentration") ) {
- if (data.components.concentration !== true) return false;
- }
- if ( filters.has("prepared") ) {
- if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
- if ( this.actor.data.type === "npc" ) return true;
- return data.preparation.prepared;
- }
-
- // Equipment-specific filters
- if ( filters.has("equipped") ) {
- if ( data.equipped !== true ) return false;
- }
- return true;
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Get the font-awesome icon used to display a certain level of skill proficiency
- * @private
- */
- _getProficiencyIcon(level) {
- const icons = {
- 0: ' ',
- 0.5: ' ',
- 1: ' ',
- 2: ' '
- };
- return icons[level] || icons[0];
- }
-
- /* -------------------------------------------- */
- /* Event Listeners and Handlers
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- activateListeners(html) {
-
- // Activate Item Filters
- const filterLists = html.find(".filter-list");
- filterLists.each(this._initializeFilterItemList.bind(this));
- filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
-
- // Item summaries
- html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
-
- // View Item Sheets
- html.find('.item-edit').click(this._onItemEdit.bind(this));
-
- // Editable Only Listeners
- if ( this.isEditable ) {
-
- // Input focus and update
- const inputs = html.find("input");
- inputs.focus(ev => ev.currentTarget.select());
- inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
-
- // Ability Proficiency
- html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
-
- // Toggle Skill Proficiency
- html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
-
- // Trait Selector
- html.find('.trait-selector').click(this._onTraitSelector.bind(this));
-
- // Configure Special Flags
- html.find('.config-button').click(this._onConfigMenu.bind(this));
-
- // Owned Item management
- html.find('.item-create').click(this._onItemCreate.bind(this));
- html.find('.item-delete').click(this._onItemDelete.bind(this));
- html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
- html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
-
- // Active Effect management
- html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
- }
-
- // Owner Only Listeners
- if ( this.actor.isOwner ) {
-
- // Ability Checks
- html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
-
-
- // Roll Skill Checks
- html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
-
- // Item Rolling
- html.find('.item .item-image').click(event => this._onItemRoll(event));
- html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
- }
-
- // Otherwise remove rollable classes
- else {
- html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
- }
-
- // Handle default listeners last so system listeners are triggered first
- super.activateListeners(html);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Iinitialize Item list filters by activating the set of filters which are currently applied
- * @private
- */
- _initializeFilterItemList(i, ul) {
- const set = this._filters[ul.dataset.filter];
- const filters = ul.querySelectorAll(".filter-item");
- for ( let li of filters ) {
- if ( set.has(li.dataset.filter) ) li.classList.add("active");
- }
- }
-
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
-
- /**
- * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
- * @param event
- * @private
- */
- _onChangeInputDelta(event) {
- const input = event.target;
- const value = input.value;
- if ( ["+", "-"].includes(value[0]) ) {
- let delta = parseFloat(value);
- input.value = getProperty(this.actor.data, input.name) + delta;
- } else if ( value[0] === "=" ) {
- input.value = value.slice(1);
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
- * @param {Event} event The click event which originated the selection
- * @private
- */
- _onConfigMenu(event) {
- event.preventDefault();
- const button = event.currentTarget;
- let app;
- switch ( button.dataset.action ) {
- case "hit-dice":
- app = new ActorHitDiceConfig(this.object);
- break;
- case "movement":
- app = new ActorMovementConfig(this.object);
- break;
- case "flags":
- app = new ActorSheetFlags(this.object);
- break;
- case "senses":
- app = new ActorSensesConfig(this.object);
- break;
- case "type":
- new ActorTypeConfig(this.object).render(true);
- break;
- }
- app?.render(true);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle cycling proficiency in a Skill
- * @param {Event} event A click or contextmenu event which triggered the handler
- * @private
- */
- _onCycleSkillProficiency(event) {
- event.preventDefault();
- const field = $(event.currentTarget).siblings('input[type="hidden"]');
-
- // Get the current level and the array of levels
- const level = parseFloat(field.val());
- const levels = [0, 1, 0.5, 2];
- let idx = levels.indexOf(level);
-
- // Toggle next level - forward on click, backwards on right
- if ( event.type === "click" ) {
- field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
- } else if ( event.type === "contextmenu" ) {
- field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
- }
-
- // Update the field value and save the form
- this._onSubmit(event);
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async _onDropActor(event, data) {
- const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
- if ( !canPolymorph ) return false;
-
- // Get the target actor
- let sourceActor = null;
- if (data.pack) {
- const pack = game.packs.find(p => p.collection === data.pack);
- sourceActor = await pack.getEntity(data.id);
- } else {
- sourceActor = game.actors.get(data.id);
- }
- if ( !sourceActor ) return;
-
- // Define a function to record polymorph settings for future use
- const rememberOptions = html => {
- const options = {};
- html.find('input').each((i, el) => {
- options[el.name] = el.checked;
- });
- const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
- game.settings.set('sw5e', 'polymorphSettings', settings);
- return settings;
- };
-
- // Create and render the Dialog
- return new Dialog({
- title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
- content: {
- options: game.settings.get('sw5e', 'polymorphSettings'),
- i18n: SW5E.polymorphSettings,
- isToken: this.actor.isToken
- },
- default: 'accept',
- buttons: {
- accept: {
- icon: ' ',
- label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
- callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
- },
- wildshape: {
- icon: ' ',
- label: game.i18n.localize('SW5E.PolymorphWildShape'),
- callback: html => this.actor.transformInto(sourceActor, {
- keepBio: true,
- keepClass: true,
- keepMental: true,
- mergeSaves: true,
- mergeSkills: true,
- transformTokens: rememberOptions(html).transformTokens
- })
- },
- polymorph: {
- icon: ' ',
- label: game.i18n.localize('SW5E.Polymorph'),
- callback: html => this.actor.transformInto(sourceActor, {
- transformTokens: rememberOptions(html).transformTokens
- })
- },
- cancel: {
- icon: ' ',
- label: game.i18n.localize('Cancel')
+ // Skills
+ if (actorData.data.skills) {
+ for (let [s, skl] of Object.entries(actorData.data.skills)) {
+ skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
+ skl.icon = this._getProficiencyIcon(skl.value);
+ skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
+ skl.label = CONFIG.SW5E.skills[s];
+ }
}
- }
- }, {
- classes: ['dialog', 'sw5e'],
- width: 600,
- template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
- }).render(true);
- }
- /* -------------------------------------------- */
+ // Movement speeds
+ data.movement = this._getMovementSpeed(actorData);
- /** @override */
- async _onDropItemCreate(itemData) {
+ // Senses
+ data.senses = this._getSenses(actorData);
- // Check to make sure items of this type are allowed on this actor
- if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
- return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
- itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
- actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
- }));
+ // Update traits
+ this._prepareTraits(actorData.data.traits);
+
+ // Prepare owned items
+ this._prepareItems(data);
+
+ // Prepare active effects
+ data.effects = prepareActiveEffectCategories(this.actor.effects);
+
+ // Return data to the sheet
+ return data;
}
- // Create a Consumable power scroll on the Inventory tab
- // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
- if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
- const scroll = await Item5e.createScrollFromPower(itemData);
- itemData = scroll.data;
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare the display of movement speed data for the Actor*
+ * @param {object} actorData The Actor data being prepared.
+ * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
+ * @returns {{primary: string, special: string}}
+ * @private
+ */
+ _getMovementSpeed(actorData, largestPrimary = false) {
+ const movement = actorData.data.attributes.movement || {};
+
+ // Prepare an array of available movement speeds
+ let 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}`]
+ ];
+ if (largestPrimary) {
+ speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
+ }
+
+ // Filter and sort speeds on their values
+ speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
+
+ // Case 1: Largest as primary
+ if (largestPrimary) {
+ let primary = speeds.shift();
+ return {
+ primary: `${primary ? primary[1] : "0"} ${movement.units}`,
+ special: speeds.map((s) => s[1]).join(", ")
+ };
+ }
+
+ // Case 2: Walk as primary
+ else {
+ return {
+ primary: `${movement.walk || 0} ${movement.units}`,
+ special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
+ };
+ }
}
- if ( itemData.data ) {
- // Ignore certain statuses
- ["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
+ /* -------------------------------------------- */
- // Downgrade ATTUNED to REQUIRED
- itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
+ _getSenses(actorData) {
+ const senses = actorData.data.attributes.senses || {};
+ const tags = {};
+ for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
+ const v = senses[k] ?? 0;
+ if (v === 0) continue;
+ tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
+ }
+ if (!!senses.special) tags["special"] = senses.special;
+ return tags;
}
- // Stack identical consumables
- if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
- const similarItem = this.actor.items.find(i => {
- const sourceId = i.getFlag("core", "sourceId");
- return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
- (i.type === "consumable");
- });
- if ( similarItem ) {
- return similarItem.update({
- 'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
+ /* -------------------------------------------- */
+
+ /**
+ * 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
+ * @private
+ */
+ _prepareTraits(traits) {
+ const map = {
+ dr: CONFIG.SW5E.damageResistanceTypes,
+ di: CONFIG.SW5E.damageResistanceTypes,
+ dv: CONFIG.SW5E.damageResistanceTypes,
+ ci: CONFIG.SW5E.conditionTypes,
+ languages: CONFIG.SW5E.languages,
+ armorProf: CONFIG.SW5E.armorProficiencies,
+ weaponProf: CONFIG.SW5E.weaponProficiencies,
+ toolProf: CONFIG.SW5E.toolProficiencies
+ };
+ for (let [t, choices] of Object.entries(map)) {
+ const trait = traits[t];
+ if (!trait) continue;
+ let values = [];
+ if (trait.value) {
+ values = trait.value instanceof Array ? trait.value : [trait.value];
+ }
+ trait.selected = values.reduce((obj, t) => {
+ obj[t] = choices[t];
+ return obj;
+ }, {});
+
+ // Add custom entry
+ if (trait.custom) {
+ trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
+ }
+ trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Insert a power into the powerbook object when rendering the character sheet
+ * @param {Object} data The Actor data being prepared
+ * @param {Array} powers The power data being prepared
+ * @private
+ */
+ _preparePowerbook(data, powers) {
+ const owner = this.actor.isOwner;
+ const levels = data.data.powers;
+ const powerbook = {};
+
+ // Define some mappings
+ const sections = {
+ atwill: -20,
+ innate: -10,
+ pact: 0.5
+ };
+
+ // Label power slot uses headers
+ const useLabels = {
+ "-20": "-",
+ "-10": "-",
+ "0": "∞"
+ };
+
+ // Format a powerbook entry for a certain indexed level
+ const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
+ powerbook[i] = {
+ order: i,
+ label: label,
+ usesSlots: i > 0,
+ canCreate: owner,
+ canPrepare: data.actor.type === "character" && i >= 1,
+ powers: [],
+ 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
+ };
+ };
+
+ // Determine the maximum power level which has a slot
+ const maxLevel = Array.fromRange(10).reduce((max, i) => {
+ if (i === 0) return max;
+ const level = levels[`power${i}`];
+ if ((level.max || level.override) && i > max) max = i;
+ return max;
+ }, 0);
+
+ // 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++) {
+ const sl = `power${lvl}`;
+ registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
+ }
+ }
+
+ // Pact magic users have cantrips and a pact magic section
+ // TODO: Check if this is needed, we've removed pacts everywhere else
+ if (levels.pact && levels.pact.max) {
+ if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
+ const l = levels.pact;
+ const config = CONFIG.SW5E.powerPreparationModes.pact;
+ const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
+ const label = `${config} — ${level}`;
+ registerSection("pact", sections.pact, label, {
+ prepMode: "pact",
+ value: l.value,
+ max: l.max,
+ override: l.override
+ });
+ }
+
+ // Iterate over every power item, adding powers to the powerbook by section
+ powers.forEach((power) => {
+ const mode = power.data.preparation.mode || "prepared";
+ let s = power.data.level || 0;
+ const sl = `power${s}`;
+
+ // Specialized powercasting modes (if they exist)
+ if (mode in sections) {
+ s = sections[mode];
+ if (!powerbook[s]) {
+ 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
+ });
+ }
+ }
+
+ // 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: levels[sl]});
+ }
+
+ // Add the power to the relevant heading
+ powerbook[s].powers.push(power);
});
- }
+
+ // Sort the powerbook by section level
+ const sorted = Object.values(powerbook);
+ sorted.sort((a, b) => a.order - b.order);
+ return sorted;
}
- // Create the owned item as normal
- return super._onDropItemCreate(itemData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Determine whether an Owned Item will be shown based on the current set of filters
+ * @return {boolean}
+ * @private
+ */
+ _filterItems(items, filters) {
+ return items.filter((item) => {
+ const data = item.data;
- /**
- * Handle enabling editing for a power slot override value
- * @param {MouseEvent} event The originating click event
- * @private
- */
- async _onPowerSlotOverride (event) {
- const span = event.currentTarget.parentElement;
- const level = span.dataset.level;
- const override = this.actor.data.data.powers[level].override || span.dataset.slots;
- const input = document.createElement("INPUT");
- input.type = "text";
- input.name = `data.powers.${level}.override`;
- input.value = override;
- input.placeholder = span.dataset.slots;
- input.dataset.dtype = "Number";
+ // Action usage
+ for (let f of ["action", "bonus", "reaction"]) {
+ if (filters.has(f)) {
+ if (data.activation && data.activation.type !== f) return false;
+ }
+ }
- // Replace the HTML
- const parent = span.parentElement;
- parent.removeChild(span);
- parent.appendChild(input);
- }
+ // Power-specific filters
+ if (filters.has("ritual")) {
+ if (data.components.ritual !== true) return false;
+ }
+ if (filters.has("concentration")) {
+ if (data.components.concentration !== true) return false;
+ }
+ if (filters.has("prepared")) {
+ if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
+ if (this.actor.data.type === "npc") return true;
+ return data.preparation.prepared;
+ }
- /* -------------------------------------------- */
-
- /**
- * Change the uses amount of an Owned Item within the Actor
- * @param {Event} event The triggering click event
- * @private
- */
- async _onUsesChange(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(itemId);
- const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
- event.target.value = uses;
- return item.update({ 'data.uses.value': uses });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
- * @private
- */
- _onItemRoll(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(itemId);
- return item.roll();
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle attempting to recharge an item usage by rolling a recharge check
- * @param {Event} event The originating click event
- * @private
- */
- _onItemRecharge(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(itemId);
- return item.rollRecharge();
- };
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
- * @private
- */
- _onItemSummary(event) {
- event.preventDefault();
- let li = $(event.currentTarget).parents(".item"),
- item = this.actor.items.get(li.data("item-id")),
- chatData = item.getChatData({secrets: this.actor.isOwner});
-
- // Toggle summary
- if ( li.hasClass("expanded") ) {
- let summary = li.children(".item-summary");
- summary.slideUp(200, () => summary.remove());
- } else {
- let div = $(`${chatData.description.value}
`);
- let props = $(`
`);
- chatData.properties.forEach(p => props.append(`${p} `));
- div.append(props);
- li.append(div.hide());
- div.slideDown(200);
+ // Equipment-specific filters
+ if (filters.has("equipped")) {
+ if (data.equipped !== true) return false;
+ }
+ return true;
+ });
}
- li.toggleClass("expanded");
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
- * @param {Event} event The originating click event
- * @private
- */
- _onItemCreate(event) {
- event.preventDefault();
- const header = event.currentTarget;
- const type = header.dataset.type;
- const itemData = {
- name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
- type: type,
- data: foundry.utils.deepClone(header.dataset)
- };
- delete itemData.data["type"];
- return this.actor.createEmbeddedDocuments("Item", [itemData]);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle editing an existing Owned Item for the Actor
- * @param {Event} event The originating click event
- * @private
- */
- _onItemEdit(event) {
- event.preventDefault();
- const li = event.currentTarget.closest(".item");
- const item = this.actor.items.get(li.dataset.itemId);
- return item.sheet.render(true);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle deleting an existing Owned Item for the Actor
- * @param {Event} event The originating click event
- * @private
- */
- _onItemDelete(event) {
- event.preventDefault();
- const li = event.currentTarget.closest(".item");
- const item = this.actor.items.get(li.dataset.itemId);
- if ( item ) return item.delete();
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling an Ability check, either a test or a saving throw
- * @param {Event} event The originating click event
- * @private
- */
- _onRollAbilityTest(event) {
- event.preventDefault();
- let ability = event.currentTarget.parentElement.dataset.ability;
- return this.actor.rollAbility(ability, {event: event});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling a Skill check
- * @param {Event} event The originating click event
- * @private
- */
- _onRollSkillCheck(event) {
- event.preventDefault();
- const skill = event.currentTarget.parentElement.dataset.skill;
- return this.actor.rollSkill(skill, {event: event});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle toggling Ability score proficiency level
- * @param {Event} event The originating click event
- * @private
- */
- _onToggleAbilityProficiency(event) {
- event.preventDefault();
- const field = event.currentTarget.previousElementSibling;
- return this.actor.update({[field.name]: 1 - parseInt(field.value)});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle toggling of filters to display a different set of owned items
- * @param {Event} event The click event which triggered the toggle
- * @private
- */
- _onToggleFilter(event) {
- event.preventDefault();
- const li = event.currentTarget;
- const set = this._filters[li.parentElement.dataset.filter];
- const filter = li.dataset.filter;
- if ( set.has(filter) ) set.delete(filter);
- else set.add(filter);
- return this.render();
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
- * @param {Event} event The click event which originated the selection
- * @private
- */
- _onTraitSelector(event) {
- event.preventDefault();
- const a = event.currentTarget;
- const label = a.parentElement.querySelector("label");
- const choices = CONFIG.SW5E[a.dataset.options];
- const options = { name: a.dataset.target, title: label.innerText, choices };
- return new TraitSelector(this.actor, options).render(true)
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- _getHeaderButtons() {
- let buttons = super._getHeaderButtons();
- if ( this.actor.isPolymorphed ) {
- buttons.unshift({
- label: 'SW5E.PolymorphRestoreTransformation',
- class: "restore-transformation",
- icon: "fas fa-backward",
- onclick: () => this.actor.revertOriginalForm()
- });
+ /**
+ * Get the font-awesome icon used to display a certain level of skill proficiency
+ * @private
+ */
+ _getProficiencyIcon(level) {
+ const icons = {
+ 0: ' ',
+ 0.5: ' ',
+ 1: ' ',
+ 2: ' '
+ };
+ return icons[level] || icons[0];
}
- return buttons;
- }
-}
\ No newline at end of file
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ activateListeners(html) {
+ // Activate Item Filters
+ const filterLists = html.find(".filter-list");
+ filterLists.each(this._initializeFilterItemList.bind(this));
+ filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
+
+ // Item summaries
+ html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
+
+ // View Item Sheets
+ html.find(".item-edit").click(this._onItemEdit.bind(this));
+
+ // Editable Only Listeners
+ if (this.isEditable) {
+ // Input focus and update
+ const inputs = html.find("input");
+ inputs.focus((ev) => ev.currentTarget.select());
+ inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
+
+ // Ability Proficiency
+ html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
+
+ // Toggle Skill Proficiency
+ html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
+
+ // Trait Selector
+ html.find(".trait-selector").click(this._onTraitSelector.bind(this));
+
+ // Configure Special Flags
+ html.find(".config-button").click(this._onConfigMenu.bind(this));
+
+ // Owned Item management
+ html.find(".item-create").click(this._onItemCreate.bind(this));
+ html.find(".item-delete").click(this._onItemDelete.bind(this));
+ html.find(".item-uses input")
+ .click((ev) => ev.target.select())
+ .change(this._onUsesChange.bind(this));
+ html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
+
+ // Active Effect management
+ html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
+ }
+
+ // Owner Only Listeners
+ if (this.actor.isOwner) {
+ // Ability Checks
+ html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
+
+ // Roll Skill Checks
+ html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
+
+ // Item Rolling
+ html.find(".item .item-image").click((event) => this._onItemRoll(event));
+ html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
+ }
+
+ // Otherwise remove rollable classes
+ else {
+ html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
+ }
+
+ // Handle default listeners last so system listeners are triggered first
+ super.activateListeners(html);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Iinitialize Item list filters by activating the set of filters which are currently applied
+ * @private
+ */
+ _initializeFilterItemList(i, ul) {
+ const set = this._filters[ul.dataset.filter];
+ const filters = ul.querySelectorAll(".filter-item");
+ for (let li of filters) {
+ if (set.has(li.dataset.filter)) li.classList.add("active");
+ }
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /**
+ * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
+ * @param event
+ * @private
+ */
+ _onChangeInputDelta(event) {
+ const input = event.target;
+ const value = input.value;
+ if (["+", "-"].includes(value[0])) {
+ let delta = parseFloat(value);
+ input.value = getProperty(this.actor.data, input.name) + delta;
+ } else if (value[0] === "=") {
+ input.value = value.slice(1);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onConfigMenu(event) {
+ event.preventDefault();
+ const button = event.currentTarget;
+ let app;
+ switch (button.dataset.action) {
+ case "hit-dice":
+ app = new ActorHitDiceConfig(this.object);
+ break;
+ case "movement":
+ app = new ActorMovementConfig(this.object);
+ break;
+ case "flags":
+ app = new ActorSheetFlags(this.object);
+ break;
+ case "senses":
+ app = new ActorSensesConfig(this.object);
+ break;
+ case "type":
+ new ActorTypeConfig(this.object).render(true);
+ break;
+ }
+ app?.render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle cycling proficiency in a Skill
+ * @param {Event} event A click or contextmenu event which triggered the handler
+ * @private
+ */
+ _onCycleSkillProficiency(event) {
+ event.preventDefault();
+ const field = $(event.currentTarget).siblings('input[type="hidden"]');
+
+ // Get the current level and the array of levels
+ const level = parseFloat(field.val());
+ const levels = [0, 1, 0.5, 2];
+ let idx = levels.indexOf(level);
+
+ // Toggle next level - forward on click, backwards on right
+ if (event.type === "click") {
+ field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
+ } else if (event.type === "contextmenu") {
+ field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
+ }
+
+ // Update the field value and save the form
+ this._onSubmit(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropActor(event, data) {
+ const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
+ if (!canPolymorph) return false;
+
+ // Get the target actor
+ let sourceActor = null;
+ if (data.pack) {
+ const pack = game.packs.find((p) => p.collection === data.pack);
+ sourceActor = await pack.getEntity(data.id);
+ } else {
+ sourceActor = game.actors.get(data.id);
+ }
+ if (!sourceActor) return;
+
+ // Define a function to record polymorph settings for future use
+ const rememberOptions = (html) => {
+ const options = {};
+ html.find("input").each((i, el) => {
+ options[el.name] = el.checked;
+ });
+ const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
+ game.settings.set("sw5e", "polymorphSettings", settings);
+ return settings;
+ };
+
+ // Create and render the Dialog
+ return new Dialog(
+ {
+ title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
+ content: {
+ options: game.settings.get("sw5e", "polymorphSettings"),
+ i18n: SW5E.polymorphSettings,
+ isToken: this.actor.isToken
+ },
+ default: "accept",
+ buttons: {
+ accept: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
+ callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
+ },
+ wildshape: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.PolymorphWildShape"),
+ callback: (html) =>
+ this.actor.transformInto(sourceActor, {
+ keepBio: true,
+ keepClass: true,
+ keepMental: true,
+ mergeSaves: true,
+ mergeSkills: true,
+ transformTokens: rememberOptions(html).transformTokens
+ })
+ },
+ polymorph: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.Polymorph"),
+ callback: (html) =>
+ this.actor.transformInto(sourceActor, {
+ transformTokens: rememberOptions(html).transformTokens
+ })
+ },
+ cancel: {
+ icon: ' ',
+ label: game.i18n.localize("Cancel")
+ }
+ }
+ },
+ {
+ classes: ["dialog", "sw5e"],
+ width: 600,
+ template: "systems/sw5e/templates/apps/polymorph-prompt.html"
+ }
+ ).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+ // Check to make sure items of this type are allowed on this actor
+ if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
+ return ui.notifications.warn(
+ game.i18n.format("SW5E.ActorWarningInvalidItem", {
+ itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
+ actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
+ })
+ );
+ }
+
+ // Create a Consumable power scroll on the Inventory tab
+ // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
+ if (itemData.type === "power" && this._tabs[0].active === "inventory") {
+ const scroll = await Item5e.createScrollFromPower(itemData);
+ itemData = scroll.data;
+ }
+
+ if (itemData.data) {
+ // Ignore certain statuses
+ ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
+
+ // Downgrade ATTUNED to REQUIRED
+ itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
+ }
+
+ // Stack identical consumables
+ if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
+ const similarItem = this.actor.items.find((i) => {
+ const sourceId = i.getFlag("core", "sourceId");
+ return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
+ });
+ if (similarItem) {
+ return similarItem.update({
+ "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
+ });
+ }
+ }
+
+ // Create the owned item as normal
+ return super._onDropItemCreate(itemData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle enabling editing for a power slot override value
+ * @param {MouseEvent} event The originating click event
+ * @private
+ */
+ async _onPowerSlotOverride(event) {
+ const span = event.currentTarget.parentElement;
+ const level = span.dataset.level;
+ const override = this.actor.data.data.powers[level].override || span.dataset.slots;
+ const input = document.createElement("INPUT");
+ input.type = "text";
+ input.name = `data.powers.${level}.override`;
+ input.value = override;
+ input.placeholder = span.dataset.slots;
+ input.dataset.dtype = "Number";
+
+ // Replace the HTML
+ const parent = span.parentElement;
+ parent.removeChild(span);
+ parent.appendChild(input);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Change the uses amount of an Owned Item within the Actor
+ * @param {Event} event The triggering click event
+ * @private
+ */
+ async _onUsesChange(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
+ event.target.value = uses;
+ return item.update({"data.uses.value": uses});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
+ * @private
+ */
+ _onItemRoll(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ return item.roll();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle attempting to recharge an item usage by rolling a recharge check
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemRecharge(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ return item.rollRecharge();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
+ * @private
+ */
+ _onItemSummary(event) {
+ event.preventDefault();
+ let li = $(event.currentTarget).parents(".item"),
+ item = this.actor.items.get(li.data("item-id")),
+ chatData = item.getChatData({secrets: this.actor.isOwner});
+
+ // Toggle summary
+ if (li.hasClass("expanded")) {
+ let summary = li.children(".item-summary");
+ summary.slideUp(200, () => summary.remove());
+ } else {
+ let div = $(`${chatData.description.value}
`);
+ let props = $(`
`);
+ chatData.properties.forEach((p) => props.append(`${p} `));
+ div.append(props);
+ li.append(div.hide());
+ div.slideDown(200);
+ }
+ li.toggleClass("expanded");
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemCreate(event) {
+ event.preventDefault();
+ const header = event.currentTarget;
+ const type = header.dataset.type;
+ const itemData = {
+ name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
+ type: type,
+ data: foundry.utils.deepClone(header.dataset)
+ };
+ delete itemData.data["type"];
+ return this.actor.createEmbeddedDocuments("Item", [itemData]);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle editing an existing Owned Item for the Actor
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemEdit(event) {
+ event.preventDefault();
+ const li = event.currentTarget.closest(".item");
+ const item = this.actor.items.get(li.dataset.itemId);
+ return item.sheet.render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle deleting an existing Owned Item for the Actor
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemDelete(event) {
+ event.preventDefault();
+ const li = event.currentTarget.closest(".item");
+ const item = this.actor.items.get(li.dataset.itemId);
+ if (item) return item.delete();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling an Ability check, either a test or a saving throw
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onRollAbilityTest(event) {
+ event.preventDefault();
+ let ability = event.currentTarget.parentElement.dataset.ability;
+ return this.actor.rollAbility(ability, {event: event});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling a Skill check
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onRollSkillCheck(event) {
+ event.preventDefault();
+ const skill = event.currentTarget.parentElement.dataset.skill;
+ return this.actor.rollSkill(skill, {event: event});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling Ability score proficiency level
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onToggleAbilityProficiency(event) {
+ event.preventDefault();
+ const field = event.currentTarget.previousElementSibling;
+ return this.actor.update({[field.name]: 1 - parseInt(field.value)});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling of filters to display a different set of owned items
+ * @param {Event} event The click event which triggered the toggle
+ * @private
+ */
+ _onToggleFilter(event) {
+ event.preventDefault();
+ const li = event.currentTarget;
+ const set = this._filters[li.parentElement.dataset.filter];
+ const filter = li.dataset.filter;
+ if (set.has(filter)) set.delete(filter);
+ else set.add(filter);
+ return this.render();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onTraitSelector(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+ const label = a.parentElement.querySelector("label");
+ const choices = CONFIG.SW5E[a.dataset.options];
+ const options = {name: a.dataset.target, title: label.innerText, choices};
+ return new TraitSelector(this.actor, options).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ _getHeaderButtons() {
+ let buttons = super._getHeaderButtons();
+ if (this.actor.isPolymorphed) {
+ buttons.unshift({
+ label: "SW5E.PolymorphRestoreTransformation",
+ class: "restore-transformation",
+ icon: "fas fa-backward",
+ onclick: () => this.actor.revertOriginalForm()
+ });
+ }
+ return buttons;
+ }
+}
diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js
index dd82ecfe..038a28b4 100644
--- a/module/actor/sheets/oldSheets/character.js
+++ b/module/actor/sheets/oldSheets/character.js
@@ -7,295 +7,362 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e}
*/
export default class ActorSheet5eCharacter extends ActorSheet5e {
+ /**
+ * Define default rendering options for the NPC sheet
+ * @return {Object}
+ */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "character"],
+ width: 720,
+ height: 736
+ });
+ }
- /**
- * Define default rendering options for the NPC sheet
- * @return {Object}
- */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["sw5e", "sheet", "actor", "character"],
- width: 720,
- height: 736
- });
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
+ */
+ getData() {
+ const sheetData = super.getData();
- /**
- * 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;
- // 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]);
+ }, []);
- // 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(", ");
+ sheetData["multiclassLabels"] = this.actor.itemTypes.class
+ .map((c) => {
+ return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
+ })
+ .join(", ");
- // Experience Tracking
- sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
- sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
- sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
- return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
- }).join(', ');
+ // Return data for rendering
+ return sheetData;
+ }
- // 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"}}
+ };
- /**
- * Organize and classify Owned Items for Character sheets
- * @private
- */
- _prepareItems(data) {
+ // Partition items by category
+ let [
+ items,
+ powers,
+ feats,
+ classes,
+ species,
+ archetypes,
+ classfeatures,
+ backgrounds,
+ fightingstyles,
+ fightingmasteries,
+ lightsaberforms
+ ] = data.items.reduce(
+ (arr, item) => {
+ // Item details
+ item.img = item.img || CONST.DEFAULT_TOKEN;
+ item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
+ item.attunement = {
+ [CONFIG.SW5E.attunementTypes.REQUIRED]: {
+ icon: "fa-sun",
+ cls: "not-attuned",
+ title: "SW5E.AttunementRequired"
+ },
+ [CONFIG.SW5E.attunementTypes.ATTUNED]: {
+ icon: "fa-sun",
+ cls: "attuned",
+ title: "SW5E.AttunementAttuned"
+ }
+ }[item.data.attunement];
- // 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"} }
- };
+ // 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);
- // Partition items by category
- let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
+ // Item toggle state
+ this._prepareItemToggleState(item);
- // Item details
- item.img = item.img || CONST.DEFAULT_TOKEN;
- item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
- item.attunement = {
- [CONFIG.SW5E.attunementTypes.REQUIRED]: {
- icon: "fa-sun",
- cls: "not-attuned",
- title: "SW5E.AttunementRequired"
- },
- [CONFIG.SW5E.attunementTypes.ATTUNED]: {
- icon: "fa-sun",
- cls: "attuned",
- title: "SW5E.AttunementAttuned"
+ // Primary Class
+ if (item.type === "class")
+ item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
+
+ // 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 === "fightingstyle") arr[8].push(item);
+ else if (item.type === "fightingmastery") arr[9].push(item);
+ else if (item.type === "lightsaberform") arr[10].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 = (i.data.quantity * i.data.weight).toNearest(0.1);
+ inventory[i.type].items.push(i);
}
- }[item.data.attunement];
- // 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));
+ // 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;
- // Item toggle state
- this._prepareItemToggleState(item);
-
- // Primary Class
- if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
-
- // 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 === "fightingstyle" ) arr[8].push(item);
- else if ( item.type === "fightingmastery" ) arr[9].push(item);
- else if ( item.type === "lightsaberform" ) arr[10].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 = (i.data.quantity * i.data.weight).toNearest(0.1);
- 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: true, 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 },
- 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.data.levels - a.data.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;
-
- // 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 {jQuery} The prepared HTML object ready to be rendered into the DOM
- */
- activateListeners(html) {
- super.activateListeners(html);
- if ( !this.isEditable ) return;
-
- // 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));
-
- // Rollable sheet actions
- html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle mouse click events for character sheet actions
- * @param {MouseEvent} event The originating click event
- * @private
- */
- _onSheetAction(event) {
- event.preventDefault();
- 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
- * @private
- */
- _onToggleItem(event) {
- event.preventDefault();
- const itemId = event.currentTarget.closest(".item").dataset.itemId;
- const item = this.actor.items.get(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();
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async _onDropItemCreate(itemData) {
-
- // Increment the number of class levels a character instead of creating a new item
- if ( itemData.type === "class" ) {
- const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
- let priorLevel = cls?.data.data.levels ?? 0;
- if ( !!cls ) {
- const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
- if ( next > priorLevel ) {
- itemData.levels = next;
- return cls.update({"data.levels": next});
+ // Organize Features
+ const features = {
+ classes: {
+ label: "SW5E.ItemTypeClassPl",
+ items: [],
+ hasActions: false,
+ dataset: {type: "class"},
+ isClass: true
+ },
+ classfeatures: {
+ label: "SW5E.ItemTypeClassFeats",
+ items: [],
+ hasActions: true,
+ 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
+ },
+ 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.data.levels - a.data.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;
+
+ // Assign and return
+ data.inventory = Object.values(inventory);
+ data.powerbook = powerbook;
+ data.preparedPowers = nPrepared;
+ data.features = Object.values(features);
}
- // Default drop handling if levels were not added
- return super._onDropItemCreate(itemData);
- }
+ /* -------------------------------------------- */
+
+ /**
+ * 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 {jQuery} The prepared HTML object ready to be rendered into the DOM
+ */
+ activateListeners(html) {
+ super.activateListeners(html);
+ if (!this.isEditable) return;
+
+ // 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));
+
+ // Rollable sheet actions
+ html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle mouse click events for character sheet actions
+ * @param {MouseEvent} event The originating click event
+ * @private
+ */
+ _onSheetAction(event) {
+ event.preventDefault();
+ 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
+ * @private
+ */
+ _onToggleItem(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(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();
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+ // Increment the number of class levels a character instead of creating a new item
+ if (itemData.type === "class") {
+ const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
+ let priorLevel = cls?.data.data.levels ?? 0;
+ if (!!cls) {
+ const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
+ if (next > priorLevel) {
+ itemData.levels = next;
+ return cls.update({"data.levels": next});
+ }
+ }
+ }
+
+ // Default drop handling if levels were not added
+ return super._onDropItemCreate(itemData);
+ }
}
diff --git a/module/actor/sheets/oldSheets/npc.js b/module/actor/sheets/oldSheets/npc.js
index 12e85b1f..ce848b0b 100644
--- a/module/actor/sheets/oldSheets/npc.js
+++ b/module/actor/sheets/oldSheets/npc.js
@@ -6,130 +6,139 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPC extends ActorSheet5e {
-
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["sw5e", "sheet", "actor", "npc"],
- width: 600,
- height: 680
- });
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- static unsupportedItemTypes = new Set(["class"]);
-
- /* -------------------------------------------- */
-
- /**
- * Organize Owned Items for rendering the NPC sheet
- * @private
- */
- _prepareItems(data) {
-
- // Categorize Items as Features and Powers
- const features = {
- weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
- actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
- passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
- equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
- };
-
- // Start by classifying items into groups for rendering
- let [powers, other] = data.items.reduce((arr, item) => {
- item.img = item.img || CONST.DEFAULT_TOKEN;
- item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
- 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));
- if ( item.type === "power" ) arr[0].push(item);
- else arr[1].push(item);
- return arr;
- }, [[], []]);
-
- // Apply item filters
- powers = this._filterItems(powers, this._filters.powerbook);
- other = this._filterItems(other, this._filters.features);
-
- // Organize Powerbook
- const powerbook = this._preparePowerbook(data, powers);
-
- // Organize Features
- for ( let item of other ) {
- if ( item.type === "weapon" ) features.weapons.items.push(item);
- else if ( item.type === "feat" ) {
- if ( item.data.activation.type ) features.actions.items.push(item);
- else features.passive.items.push(item);
- }
- else features.equipment.items.push(item);
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "npc"],
+ width: 600,
+ height: 680
+ });
}
- // Assign and return
- data.features = Object.values(features);
- data.powerbook = powerbook;
- }
+ /* -------------------------------------------- */
+ /** @override */
+ static unsupportedItemTypes = new Set(["class"]);
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @inheritdoc */
- getData(options) {
- const data = super.getData(options);
+ /**
+ * Organize Owned Items for rendering the NPC sheet
+ * @private
+ */
+ _prepareItems(data) {
+ // Categorize Items as Features and Powers
+ const features = {
+ weapons: {
+ label: game.i18n.localize("SW5E.AttackPl"),
+ items: [],
+ hasActions: true,
+ dataset: {"type": "weapon", "weapon-type": "natural"}
+ },
+ actions: {
+ label: game.i18n.localize("SW5E.ActionPl"),
+ items: [],
+ hasActions: true,
+ dataset: {"type": "feat", "activation.type": "action"}
+ },
+ passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
+ equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
+ };
- // Challenge Rating
- const cr = parseFloat(data.data.details.cr || 0);
- const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
- data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
+ // Start by classifying items into groups for rendering
+ let [powers, other] = data.items.reduce(
+ (arr, item) => {
+ item.img = item.img || CONST.DEFAULT_TOKEN;
+ item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
+ 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);
+ if (item.type === "power") arr[0].push(item);
+ else arr[1].push(item);
+ return arr;
+ },
+ [[], []]
+ );
- // Creature Type
- data.labels["type"] = this.actor.labels.creatureType;
- return data;
- }
+ // Apply item filters
+ powers = this._filterItems(powers, this._filters.powerbook);
+ other = this._filterItems(other, this._filters.features);
- /* -------------------------------------------- */
- /* Object Updates */
- /* -------------------------------------------- */
+ // Organize Powerbook
+ const powerbook = this._preparePowerbook(data, powers);
- /** @override */
- async _updateObject(event, formData) {
+ // Organize Features
+ for (let item of other) {
+ if (item.type === "weapon") features.weapons.items.push(item);
+ else if (item.type === "feat") {
+ if (item.data.activation.type) features.actions.items.push(item);
+ else features.passive.items.push(item);
+ } else features.equipment.items.push(item);
+ }
- // Format NPC Challenge Rating
- const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
- let crv = "data.details.cr";
- let cr = formData[crv];
- cr = crs[cr] || parseFloat(cr);
- if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
+ // Assign and return
+ data.features = Object.values(features);
+ data.powerbook = powerbook;
+ }
- // Parent ActorSheet update steps
- return super._updateObject(event, formData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
+ /** @inheritdoc */
+ getData(options) {
+ const data = super.getData(options);
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
- html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
- }
+ // Challenge Rating
+ const cr = parseFloat(data.data.details.cr || 0);
+ const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
+ data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
- /* -------------------------------------------- */
+ // Creature Type
+ data.labels["type"] = this.actor.labels.creatureType;
+ return data;
+ }
- /**
- * Handle rolling NPC health values using the provided formula
- * @param {Event} event The original click event
- * @private
- */
- _onRollHPFormula(event) {
- event.preventDefault();
- const formula = this.actor.data.data.attributes.hp.formula;
- if ( !formula ) return;
- const hp = new Roll(formula).roll().total;
- AudioHelper.play({src: CONFIG.sounds.dice});
- this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
- }
+ /* -------------------------------------------- */
+ /* Object Updates */
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _updateObject(event, formData) {
+ // Format NPC Challenge Rating
+ const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
+ let crv = "data.details.cr";
+ let cr = formData[crv];
+ cr = crs[cr] || parseFloat(cr);
+ if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
+
+ // Parent ActorSheet update steps
+ return super._updateObject(event, formData);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling NPC health values using the provided formula
+ * @param {Event} event The original click event
+ * @private
+ */
+ _onRollHPFormula(event) {
+ event.preventDefault();
+ const formula = this.actor.data.data.attributes.hp.formula;
+ if (!formula) return;
+ const hp = new Roll(formula).roll().total;
+ AudioHelper.play({src: CONFIG.sounds.dice});
+ this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
+ }
}
diff --git a/module/actor/sheets/oldSheets/vehicle.js b/module/actor/sheets/oldSheets/vehicle.js
index b6ec2fc8..a5c6e2ea 100644
--- a/module/actor/sheets/oldSheets/vehicle.js
+++ b/module/actor/sheets/oldSheets/vehicle.js
@@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js";
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
- /**
- * Define default rendering options for the Vehicle sheet.
- * @returns {Object}
- */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["sw5e", "sheet", "actor", "vehicle"],
- width: 605,
- height: 680
- });
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- static unsupportedItemTypes = new Set(["class"]);
-
- /* -------------------------------------------- */
-
-
- /**
- * Creates a new cargo entry for a vehicle Actor.
- */
- static get newCargo() {
- return {
- name: '',
- quantity: 1
- };
- }
-
- /* -------------------------------------------- */
-
- /**
- * Compute the total weight of the vehicle's cargo.
- * @param {Number} totalWeight The cumulative item weight from inventory items
- * @param {Object} actorData The data object for the Actor being rendered
- * @returns {{max: number, value: number, pct: number}}
- * @private
- */
- _computeEncumbrance(totalWeight, actorData) {
-
- // Compute currency weight
- const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
- totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
-
- // Vehicle weights are an order of magnitude greater.
- totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
-
- // Compute overall encumbrance
- const max = actorData.data.attributes.capacity.cargo;
- const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
- return {value: totalWeight.toNearest(0.1), max, pct};
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- _getMovementSpeed(actorData, largestPrimary=true) {
- return super._getMovementSpeed(actorData, largestPrimary);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare items that are mounted to a vehicle and require one or more crew
- * to operate.
- * @private
- */
- _prepareCrewedItem(item) {
-
- // Determine crewed status
- const isCrewed = item.data.crewed;
- item.toggleClass = isCrewed ? 'active' : '';
- item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
-
- // Handle crew actions
- if (item.type === 'feat' && item.data.activation.type === 'crew') {
- item.crew = item.data.activation.cost;
- item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
- if (item.data.cover === .5) item.cover = '½';
- else if (item.data.cover === .75) item.cover = '¾';
- else if (item.data.cover === null) item.cover = '—';
- if (item.crew < 1 || item.crew === null) item.crew = '—';
+ /**
+ * Define default rendering options for the Vehicle sheet.
+ * @returns {Object}
+ */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "vehicle"],
+ width: 605,
+ height: 680
+ });
}
- // Prepare vehicle weapons
- if (item.type === 'equipment' || item.type === 'weapon') {
- item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
- }
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /** @override */
+ static unsupportedItemTypes = new Set(["class"]);
- /**
- * Organize Owned Items for rendering the Vehicle sheet.
- * @private
- */
- _prepareItems(data) {
- const cargoColumns = [{
- label: game.i18n.localize('SW5E.Quantity'),
- css: 'item-qty',
- property: 'quantity',
- editable: 'Number'
- }];
+ /* -------------------------------------------- */
- const equipmentColumns = [{
- label: game.i18n.localize('SW5E.Quantity'),
- css: 'item-qty',
- property: 'data.quantity'
- }, {
- label: game.i18n.localize('SW5E.AC'),
- css: 'item-ac',
- property: 'data.armor.value'
- }, {
- label: game.i18n.localize('SW5E.HP'),
- css: 'item-hp',
- property: 'data.hp.value',
- editable: 'Number'
- }, {
- label: game.i18n.localize('SW5E.Threshold'),
- css: 'item-threshold',
- property: 'threshold'
- }];
-
- const features = {
- actions: {
- label: game.i18n.localize('SW5E.ActionPl'),
- items: [],
- crewable: true,
- dataset: {type: 'feat', 'activation.type': 'crew'},
- columns: [{
- label: game.i18n.localize('SW5E.VehicleCrew'),
- css: 'item-crew',
- property: 'crew'
- }, {
- label: game.i18n.localize('SW5E.Cover'),
- css: 'item-cover',
- property: 'cover'
- }]
- },
- equipment: {
- label: game.i18n.localize('SW5E.ItemTypeEquipment'),
- items: [],
- crewable: true,
- dataset: {type: 'equipment', 'armor.type': 'vehicle'},
- columns: equipmentColumns
- },
- passive: {
- label: game.i18n.localize('SW5E.Features'),
- items: [],
- dataset: {type: 'feat'}
- },
- reactions: {
- label: game.i18n.localize('SW5E.ReactionPl'),
- items: [],
- dataset: {type: 'feat', 'activation.type': 'reaction'}
- },
- weapons: {
- label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
- items: [],
- crewable: true,
- dataset: {type: 'weapon', 'weapon-type': 'siege'},
- columns: equipmentColumns
- }
- };
-
- const cargo = {
- crew: {
- label: game.i18n.localize('SW5E.VehicleCrew'),
- items: data.data.cargo.crew,
- css: 'cargo-row crew',
- editableName: true,
- dataset: {type: 'crew'},
- columns: cargoColumns
- },
- passengers: {
- label: game.i18n.localize('SW5E.VehiclePassengers'),
- items: data.data.cargo.passengers,
- css: 'cargo-row passengers',
- editableName: true,
- dataset: {type: 'passengers'},
- columns: cargoColumns
- },
- cargo: {
- label: game.i18n.localize('SW5E.VehicleCargo'),
- items: [],
- dataset: {type: 'loot'},
- columns: [{
- label: game.i18n.localize('SW5E.Quantity'),
- css: 'item-qty',
- property: 'data.quantity',
- editable: 'Number'
- }, {
- label: game.i18n.localize('SW5E.Price'),
- css: 'item-price',
- property: 'data.price',
- editable: 'Number'
- }, {
- label: game.i18n.localize('SW5E.Weight'),
- css: 'item-weight',
- property: 'data.weight',
- editable: 'Number'
- }]
- }
- };
-
- // Classify items owned by the vehicle and compute total cargo weight
- let totalWeight = 0;
- for (const item of data.items) {
- this._prepareCrewedItem(item);
-
- // Handle cargo explicitly
- const isCargo = item.flags.sw5e?.vehicleCargo === true;
- if ( isCargo ) {
- totalWeight += (item.data.weight || 0) * item.data.quantity;
- cargo.cargo.items.push(item);
- continue;
- }
-
- // Handle non-cargo item types
- switch ( item.type ) {
- case "weapon":
- features.weapons.items.push(item);
- break;
- case "equipment":
- features.equipment.items.push(item);
- break;
- case "feat":
- if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item);
- else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
- else features.actions.items.push(item);
- break;
- default:
- totalWeight += (item.data.weight || 0) * item.data.quantity;
- cargo.cargo.items.push(item);
- }
+ /**
+ * Creates a new cargo entry for a vehicle Actor.
+ */
+ static get newCargo() {
+ return {
+ name: "",
+ quantity: 1
+ };
}
- // Update the rendering context data
- data.features = Object.values(features);
- data.cargo = Object.values(cargo);
- data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
- /* Event Listeners and Handlers */
- /* -------------------------------------------- */
+ /**
+ * Compute the total weight of the vehicle's cargo.
+ * @param {Number} totalWeight The cumulative item weight from inventory items
+ * @param {Object} actorData The data object for the Actor being rendered
+ * @returns {{max: number, value: number, pct: number}}
+ * @private
+ */
+ _computeEncumbrance(totalWeight, actorData) {
+ // Compute currency weight
+ const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
+ totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
- if (!this.isEditable) return;
+ // Vehicle weights are an order of magnitude greater.
+ totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
- html.find('.item-toggle').click(this._onToggleItem.bind(this));
- html.find('.item-hp input')
- .click(evt => evt.target.select())
- .change(this._onHPChange.bind(this));
-
- html.find('.item:not(.cargo-row) input[data-property]')
- .click(evt => evt.target.select())
- .change(this._onEditInSheet.bind(this));
-
- html.find('.cargo-row input')
- .click(evt => evt.target.select())
- .change(this._onCargoRowChange.bind(this));
-
- if (this.actor.data.data.attributes.actions.stations) {
- html.find('.counter.actions, .counter.action-thresholds').hide();
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
- * @param event {Event}
- * @returns {Promise|null}
- * @private
- */
- _onCargoRowChange(event) {
- event.preventDefault();
- const target = event.currentTarget;
- const row = target.closest('.item');
- const idx = Number(row.dataset.itemId);
- const property = row.classList.contains('crew') ? 'crew' : 'passengers';
-
- // Get the cargo entry
- const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
- const entry = cargo[idx];
- if (!entry) return null;
-
- // Update the cargo value
- const key = target.dataset.property || 'name';
- const type = target.dataset.dtype;
- let value = target.value;
- if (type === 'Number') value = Number(value);
- entry[key] = value;
-
- // Perform the Actor update
- return this.actor.update({[`data.cargo.${property}`]: cargo});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle editing certain values like quantity, price, and weight in-sheet.
- * @param event {Event}
- * @returns {Promise- }
- * @private
- */
- _onEditInSheet(event) {
- event.preventDefault();
- const itemID = event.currentTarget.closest('.item').dataset.itemId;
- const item = this.actor.items.get(itemID);
- const property = event.currentTarget.dataset.property;
- const type = event.currentTarget.dataset.dtype;
- let value = event.currentTarget.value;
- switch (type) {
- case 'Number': value = parseInt(value); break;
- case 'Boolean': value = value === 'true'; break;
- }
- return item.update({[`${property}`]: value});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle creating a new crew or passenger row.
- * @param event {Event}
- * @returns {Promise
}
- * @private
- */
- _onItemCreate(event) {
- event.preventDefault();
- const target = event.currentTarget;
- const type = target.dataset.type;
- if (type === 'crew' || type === 'passengers') {
- const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
- cargo.push(this.constructor.newCargo);
- return this.actor.update({[`data.cargo.${type}`]: cargo});
- }
- return super._onItemCreate(event);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle deleting a crew or passenger row.
- * @param event {Event}
- * @returns {Promise}
- * @private
- */
- _onItemDelete(event) {
- event.preventDefault();
- const row = event.currentTarget.closest('.item');
- if (row.classList.contains('cargo-row')) {
- const idx = Number(row.dataset.itemId);
- const type = row.classList.contains('crew') ? 'crew' : 'passengers';
- const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
- return this.actor.update({[`data.cargo.${type}`]: cargo});
+ // Compute overall encumbrance
+ const max = actorData.data.attributes.capacity.cargo;
+ const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
+ return {value: totalWeight.toNearest(0.1), max, pct};
}
- return super._onItemDelete(event);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /** @override */
+ _getMovementSpeed(actorData, largestPrimary = true) {
+ return super._getMovementSpeed(actorData, largestPrimary);
+ }
- /** @override */
- async _onDropItemCreate(itemData) {
- const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
- const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
- foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
- return super._onDropItemCreate(itemData);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Prepare items that are mounted to a vehicle and require one or more crew
+ * to operate.
+ * @private
+ */
+ _prepareCrewedItem(item) {
+ // Determine crewed status
+ const isCrewed = item.data.crewed;
+ item.toggleClass = isCrewed ? "active" : "";
+ item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
- /**
- * Special handling for editing HP to clamp it within appropriate range.
- * @param event {Event}
- * @returns {Promise- }
- * @private
- */
- _onHPChange(event) {
- event.preventDefault();
- const itemID = event.currentTarget.closest('.item').dataset.itemId;
- const item = this.actor.items.get(itemID);
- const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
- event.currentTarget.value = hp;
- return item.update({'data.hp.value': hp});
- }
+ // Handle crew actions
+ if (item.type === "feat" && item.data.activation.type === "crew") {
+ item.crew = item.data.activation.cost;
+ item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
+ if (item.data.cover === 0.5) item.cover = "½";
+ else if (item.data.cover === 0.75) item.cover = "¾";
+ else if (item.data.cover === null) item.cover = "—";
+ if (item.crew < 1 || item.crew === null) item.crew = "—";
+ }
- /* -------------------------------------------- */
+ // Prepare vehicle weapons
+ if (item.type === "equipment" || item.type === "weapon") {
+ item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
+ }
+ }
- /**
- * Handle toggling an item's crewed status.
- * @param event {Event}
- * @returns {Promise
- }
- * @private
- */
- _onToggleItem(event) {
- event.preventDefault();
- const itemID = event.currentTarget.closest('.item').dataset.itemId;
- const item = this.actor.items.get(itemID);
- const crewed = !!item.data.data.crewed;
- return item.update({'data.crewed': !crewed});
- }
-};
+ /* -------------------------------------------- */
+
+ /**
+ * Organize Owned Items for rendering the Vehicle sheet.
+ * @private
+ */
+ _prepareItems(data) {
+ const cargoColumns = [
+ {
+ label: game.i18n.localize("SW5E.Quantity"),
+ css: "item-qty",
+ property: "quantity",
+ editable: "Number"
+ }
+ ];
+
+ const equipmentColumns = [
+ {
+ label: game.i18n.localize("SW5E.Quantity"),
+ css: "item-qty",
+ property: "data.quantity"
+ },
+ {
+ label: game.i18n.localize("SW5E.AC"),
+ css: "item-ac",
+ property: "data.armor.value"
+ },
+ {
+ label: game.i18n.localize("SW5E.HP"),
+ css: "item-hp",
+ property: "data.hp.value",
+ editable: "Number"
+ },
+ {
+ label: game.i18n.localize("SW5E.Threshold"),
+ css: "item-threshold",
+ property: "threshold"
+ }
+ ];
+
+ const features = {
+ actions: {
+ label: game.i18n.localize("SW5E.ActionPl"),
+ items: [],
+ crewable: true,
+ dataset: {"type": "feat", "activation.type": "crew"},
+ columns: [
+ {
+ label: game.i18n.localize("SW5E.VehicleCrew"),
+ css: "item-crew",
+ property: "crew"
+ },
+ {
+ label: game.i18n.localize("SW5E.Cover"),
+ css: "item-cover",
+ property: "cover"
+ }
+ ]
+ },
+ equipment: {
+ label: game.i18n.localize("SW5E.ItemTypeEquipment"),
+ items: [],
+ crewable: true,
+ dataset: {"type": "equipment", "armor.type": "vehicle"},
+ columns: equipmentColumns
+ },
+ passive: {
+ label: game.i18n.localize("SW5E.Features"),
+ items: [],
+ dataset: {type: "feat"}
+ },
+ reactions: {
+ label: game.i18n.localize("SW5E.ReactionPl"),
+ items: [],
+ dataset: {"type": "feat", "activation.type": "reaction"}
+ },
+ weapons: {
+ label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
+ items: [],
+ crewable: true,
+ dataset: {"type": "weapon", "weapon-type": "siege"},
+ columns: equipmentColumns
+ }
+ };
+
+ const cargo = {
+ crew: {
+ label: game.i18n.localize("SW5E.VehicleCrew"),
+ items: data.data.cargo.crew,
+ css: "cargo-row crew",
+ editableName: true,
+ dataset: {type: "crew"},
+ columns: cargoColumns
+ },
+ passengers: {
+ label: game.i18n.localize("SW5E.VehiclePassengers"),
+ items: data.data.cargo.passengers,
+ css: "cargo-row passengers",
+ editableName: true,
+ dataset: {type: "passengers"},
+ columns: cargoColumns
+ },
+ cargo: {
+ label: game.i18n.localize("SW5E.VehicleCargo"),
+ items: [],
+ dataset: {type: "loot"},
+ columns: [
+ {
+ label: game.i18n.localize("SW5E.Quantity"),
+ css: "item-qty",
+ property: "data.quantity",
+ editable: "Number"
+ },
+ {
+ label: game.i18n.localize("SW5E.Price"),
+ css: "item-price",
+ property: "data.price",
+ editable: "Number"
+ },
+ {
+ label: game.i18n.localize("SW5E.Weight"),
+ css: "item-weight",
+ property: "data.weight",
+ editable: "Number"
+ }
+ ]
+ }
+ };
+
+ // Classify items owned by the vehicle and compute total cargo weight
+ let totalWeight = 0;
+ for (const item of data.items) {
+ this._prepareCrewedItem(item);
+
+ // Handle cargo explicitly
+ const isCargo = item.flags.sw5e?.vehicleCargo === true;
+ if (isCargo) {
+ totalWeight += (item.data.weight || 0) * item.data.quantity;
+ cargo.cargo.items.push(item);
+ continue;
+ }
+
+ // Handle non-cargo item types
+ switch (item.type) {
+ case "weapon":
+ features.weapons.items.push(item);
+ break;
+ case "equipment":
+ features.equipment.items.push(item);
+ break;
+ case "feat":
+ if (!item.data.activation.type || item.data.activation.type === "none")
+ features.passive.items.push(item);
+ else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
+ else features.actions.items.push(item);
+ break;
+ default:
+ totalWeight += (item.data.weight || 0) * item.data.quantity;
+ cargo.cargo.items.push(item);
+ }
+ }
+
+ // Update the rendering context data
+ data.features = Object.values(features);
+ data.cargo = Object.values(cargo);
+ data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ if (!this.isEditable) return;
+
+ html.find(".item-toggle").click(this._onToggleItem.bind(this));
+ html.find(".item-hp input")
+ .click((evt) => evt.target.select())
+ .change(this._onHPChange.bind(this));
+
+ html.find(".item:not(.cargo-row) input[data-property]")
+ .click((evt) => evt.target.select())
+ .change(this._onEditInSheet.bind(this));
+
+ html.find(".cargo-row input")
+ .click((evt) => evt.target.select())
+ .change(this._onCargoRowChange.bind(this));
+
+ if (this.actor.data.data.attributes.actions.stations) {
+ html.find(".counter.actions, .counter.action-thresholds").hide();
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
+ * @param event {Event}
+ * @returns {Promise
|null}
+ * @private
+ */
+ _onCargoRowChange(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ const row = target.closest(".item");
+ const idx = Number(row.dataset.itemId);
+ const property = row.classList.contains("crew") ? "crew" : "passengers";
+
+ // Get the cargo entry
+ const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
+ const entry = cargo[idx];
+ if (!entry) return null;
+
+ // Update the cargo value
+ const key = target.dataset.property || "name";
+ const type = target.dataset.dtype;
+ let value = target.value;
+ if (type === "Number") value = Number(value);
+ entry[key] = value;
+
+ // Perform the Actor update
+ return this.actor.update({[`data.cargo.${property}`]: cargo});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle editing certain values like quantity, price, and weight in-sheet.
+ * @param event {Event}
+ * @returns {Promise- }
+ * @private
+ */
+ _onEditInSheet(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const property = event.currentTarget.dataset.property;
+ const type = event.currentTarget.dataset.dtype;
+ let value = event.currentTarget.value;
+ switch (type) {
+ case "Number":
+ value = parseInt(value);
+ break;
+ case "Boolean":
+ value = value === "true";
+ break;
+ }
+ return item.update({[`${property}`]: value});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle creating a new crew or passenger row.
+ * @param event {Event}
+ * @returns {Promise
}
+ * @private
+ */
+ _onItemCreate(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ const type = target.dataset.type;
+ if (type === "crew" || type === "passengers") {
+ const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
+ cargo.push(this.constructor.newCargo);
+ return this.actor.update({[`data.cargo.${type}`]: cargo});
+ }
+ return super._onItemCreate(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle deleting a crew or passenger row.
+ * @param event {Event}
+ * @returns {Promise}
+ * @private
+ */
+ _onItemDelete(event) {
+ event.preventDefault();
+ const row = event.currentTarget.closest(".item");
+ if (row.classList.contains("cargo-row")) {
+ const idx = Number(row.dataset.itemId);
+ const type = row.classList.contains("crew") ? "crew" : "passengers";
+ const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
+ return this.actor.update({[`data.cargo.${type}`]: cargo});
+ }
+
+ return super._onItemDelete(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+ const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
+ const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
+ foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
+ return super._onDropItemCreate(itemData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Special handling for editing HP to clamp it within appropriate range.
+ * @param event {Event}
+ * @returns {Promise- }
+ * @private
+ */
+ _onHPChange(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
+ event.currentTarget.value = hp;
+ return item.update({"data.hp.value": hp});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling an item's crewed status.
+ * @param event {Event}
+ * @returns {Promise
- }
+ * @private
+ */
+ _onToggleItem(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const crewed = !!item.data.data.crewed;
+ return item.update({"data.crewed": !crewed});
+ }
+}
diff --git a/module/apps/ability-use-dialog.js b/module/apps/ability-use-dialog.js
index 4b92b14d..6c43775d 100644
--- a/module/apps/ability-use-dialog.js
+++ b/module/apps/ability-use-dialog.js
@@ -3,220 +3,225 @@
* @type {Dialog}
*/
export default class AbilityUseDialog extends Dialog {
- constructor(item, dialogData={}, options={}) {
- super(dialogData, options);
- this.options.classes = ["sw5e", "dialog"];
+ constructor(item, dialogData = {}, options = {}) {
+ super(dialogData, options);
+ this.options.classes = ["sw5e", "dialog"];
+
+ /**
+ * Store a reference to the Item entity being used
+ * @type {Item5e}
+ */
+ this.item = item;
+ }
+
+ /* -------------------------------------------- */
+ /* Rendering */
+ /* -------------------------------------------- */
/**
- * Store a reference to the Item entity being used
- * @type {Item5e}
+ * 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 {Item5e} item
+ * @return {Promise}
*/
- this.item = item;
- }
+ static async create(item) {
+ if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
- /* -------------------------------------------- */
- /* Rendering */
- /* -------------------------------------------- */
+ // Prepare data
+ const actorData = item.actor.data.data;
+ const itemData = item.data.data;
+ const uses = itemData.uses || {};
+ const quantity = itemData.quantity || 0;
+ const recharge = itemData.recharge || {};
+ const recharges = !!recharge.value;
+ const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
- /**
- * 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 {Item5e} item
- * @return {Promise}
- */
- static async create(item) {
- if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
+ // Prepare dialog form data
+ const data = {
+ item: item.data,
+ title: game.i18n.format("SW5E.AbilityUseHint", {
+ type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
+ name: item.name
+ }),
+ note: this._getAbilityUseNote(item.data, uses, recharge),
+ consumePowerSlot: false,
+ consumeRecharge: recharges,
+ consumeResource: !!itemData.consume.target,
+ consumeUses: uses.per && uses.max > 0,
+ canUse: recharges ? recharge.charged : sufficientUses,
+ createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
+ errors: []
+ };
+ if (item.data.type === "power") this._getPowerData(actorData, itemData, data);
- // Prepare data
- const actorData = item.actor.data.data;
- const itemData = item.data.data;
- const uses = itemData.uses || {};
- const quantity = itemData.quantity || 0;
- const recharge = itemData.recharge || {};
- const recharges = !!recharge.value;
- const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
+ // Render the ability usage template
+ const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
- // Prepare dialog form data
- const data = {
- item: item.data,
- title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}),
- note: this._getAbilityUseNote(item.data, uses, recharge),
- consumePowerSlot: false,
- consumeRecharge: recharges,
- consumeResource: !!itemData.consume.target,
- consumeUses: uses.per && (uses.max > 0),
- canUse: recharges ? recharge.charged : sufficientUses,
- createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
- errors: []
- };
- if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
+ // Create the Dialog and return data as a Promise
+ const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
+ const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
+ return new Promise((resolve) => {
+ const dlg = new this(item, {
+ title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
+ content: html,
+ buttons: {
+ use: {
+ icon: `
`,
+ label: label,
+ callback: (html) => {
+ const fd = new FormDataExtended(html[0].querySelector("form"));
+ resolve(fd.toObject());
+ }
+ }
+ },
+ default: "use",
+ close: () => resolve(null)
+ });
+ dlg.render(true);
+ });
+ }
- // Render the ability usage template
- const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
+ /* -------------------------------------------- */
+ /* Helpers */
+ /* -------------------------------------------- */
- // Create the Dialog and return data as a Promise
- const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
- const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
- return new Promise((resolve) => {
- const dlg = new this(item, {
- title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
- content: html,
- buttons: {
- use: {
- icon: ` `,
- label: label,
- callback: html => {
- const fd = new FormDataExtended(html[0].querySelector("form"));
- resolve(fd.toObject());
+ /**
+ * Get dialog data related to limited power slots
+ * @private
+ */
+ static _getPowerData(actorData, itemData, data) {
+ // Determine whether the power may be up-cast
+ const lvl = itemData.level;
+ const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
+
+ // If can't upcast, return early and don't bother calculating available power slots
+ if (!consumePowerSlot) {
+ mergeObject(data, {isPower: true, consumePowerSlot});
+ return;
+ }
+
+ // Determine the levels which are feasible
+ let lmax = 0;
+ let points;
+ let powerType;
+ switch (itemData.school) {
+ case "lgt":
+ case "uni":
+ case "drk": {
+ powerType = "force";
+ points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
+ break;
+ }
+ case "tec": {
+ powerType = "tech";
+ points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
+ break;
}
- }
- },
- default: "use",
- close: () => resolve(null)
- });
- dlg.render(true);
- });
- }
-
- /* -------------------------------------------- */
- /* Helpers */
- /* -------------------------------------------- */
-
- /**
- * Get dialog data related to limited power slots
- * @private
- */
- static _getPowerData(actorData, itemData, data) {
-
- // Determine whether the power may be up-cast
- const lvl = itemData.level;
- const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
-
- // If can't upcast, return early and don't bother calculating available power slots
- if (!consumePowerSlot) {
- mergeObject(data, { isPower: true, consumePowerSlot });
- return;
- }
-
- // Determine the levels which are feasible
- let lmax = 0;
- let points;
- let powerType;
- switch (itemData.school){
- case "lgt":
- case "uni":
- case "drk": {
- powerType = "force"
- points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
- break;
- }
- case "tec": {
- powerType = "tech"
- points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
- break;
- }
- }
-
- // eliminate point usage for innate casters
- if (actorData.attributes.powercasting === 'innate') points = 999;
-
-
- let powerLevels
- if (powerType === "force"){
- powerLevels = Array.fromRange(10).reduce((arr, i) => {
- if ( i < lvl ) return arr;
- const label = CONFIG.SW5E.powerLevels[i];
- const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
- let max = parseInt(l.foverride || l.fmax || 0);
- let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
- if ( max > 0 ) lmax = i;
- if ((max > 0) && (slots > 0) && (points > i)){
- arr.push({
- level: i,
- label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
- canCast: max > 0,
- hasSlots: slots > 0
- });
}
- return arr;
- }, []).filter(sl => sl.level <= lmax);
- }else if (powerType === "tech"){
- powerLevels = Array.fromRange(10).reduce((arr, i) => {
- if ( i < lvl ) return arr;
- const label = CONFIG.SW5E.powerLevels[i];
- const l = actorData.powers["power"+i] || {tmax: 0, toverride: null};
- let max = parseInt(l.override || l.tmax || 0);
- let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
- if ( max > 0 ) lmax = i;
- if ((max > 0) && (slots > 0) && (points > i)){
- arr.push({
- level: i,
- label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
- canCast: max > 0,
- hasSlots: slots > 0
- });
+
+ // eliminate point usage for innate casters
+ if (actorData.attributes.powercasting === "innate") points = 999;
+
+ let powerLevels;
+ if (powerType === "force") {
+ powerLevels = Array.fromRange(10)
+ .reduce((arr, i) => {
+ if (i < lvl) return arr;
+ const label = CONFIG.SW5E.powerLevels[i];
+ const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
+ let max = parseInt(l.foverride || l.fmax || 0);
+ let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
+ if (max > 0) lmax = i;
+ if (max > 0 && slots > 0 && points > i) {
+ arr.push({
+ level: i,
+ label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
+ canCast: max > 0,
+ hasSlots: slots > 0
+ });
+ }
+ return arr;
+ }, [])
+ .filter((sl) => sl.level <= lmax);
+ } else if (powerType === "tech") {
+ powerLevels = Array.fromRange(10)
+ .reduce((arr, i) => {
+ if (i < lvl) return arr;
+ const label = CONFIG.SW5E.powerLevels[i];
+ const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
+ let max = parseInt(l.override || l.tmax || 0);
+ let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
+ if (max > 0) lmax = i;
+ if (max > 0 && slots > 0 && points > i) {
+ arr.push({
+ level: i,
+ label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
+ canCast: max > 0,
+ hasSlots: slots > 0
+ });
+ }
+ return arr;
+ }, [])
+ .filter((sl) => sl.level <= lmax);
}
- return arr;
- }, []).filter(sl => sl.level <= lmax);
- }
-
-
- const canCast = powerLevels.some(l => l.hasSlots);
- if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
- level: CONFIG.SW5E.powerLevels[lvl],
- name: data.item.name
- }));
+ const canCast = powerLevels.some((l) => l.hasSlots);
+ if (!canCast)
+ data.errors.push(
+ game.i18n.format("SW5E.PowerCastNoSlots", {
+ level: CONFIG.SW5E.powerLevels[lvl],
+ name: data.item.name
+ })
+ );
- // Merge power casting data
- return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Get the ability usage note that is displayed
- * @private
- */
- static _getAbilityUseNote(item, uses, recharge) {
-
- // Zero quantity
- const quantity = item.data.quantity;
- if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
-
- // Abilities which use Recharge
- if ( !!recharge.value ) {
- return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
- type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
- })
+ // Merge power casting data
+ return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
}
- // Does not use any resource
- if ( !uses.per || !uses.max ) return "";
+ /* -------------------------------------------- */
- // Consumables
- if ( item.type === "consumable" ) {
- let str = "SW5E.AbilityUseNormalHint";
- if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint";
- else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
- else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
- return game.i18n.format(str, {
- type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
- value: uses.value,
- quantity: item.data.quantity,
- max: uses.max,
- per: CONFIG.SW5E.limitedUsePeriods[uses.per]
- });
- }
+ /**
+ * Get the ability usage note that is displayed
+ * @private
+ */
+ static _getAbilityUseNote(item, uses, recharge) {
+ // Zero quantity
+ const quantity = item.data.quantity;
+ if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
- // Other Items
- else {
- return game.i18n.format("SW5E.AbilityUseNormalHint", {
- type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
- value: uses.value,
- max: uses.max,
- per: CONFIG.SW5E.limitedUsePeriods[uses.per]
- });
+ // Abilities which use Recharge
+ if (!!recharge.value) {
+ return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
+ type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
+ });
+ }
+
+ // Does not use any resource
+ if (!uses.per || !uses.max) return "";
+
+ // Consumables
+ if (item.type === "consumable") {
+ let str = "SW5E.AbilityUseNormalHint";
+ if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint";
+ else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
+ else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
+ return game.i18n.format(str, {
+ type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
+ value: uses.value,
+ quantity: item.data.quantity,
+ max: uses.max,
+ per: CONFIG.SW5E.limitedUsePeriods[uses.per]
+ });
+ }
+
+ // Other Items
+ else {
+ return game.i18n.format("SW5E.AbilityUseNormalHint", {
+ type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
+ value: uses.value,
+ max: uses.max,
+ per: CONFIG.SW5E.limitedUsePeriods[uses.per]
+ });
+ }
}
- }
}
diff --git a/module/apps/actor-flags.js b/module/apps/actor-flags.js
index cb3a816c..8f5255a5 100644
--- a/module/apps/actor-flags.js
+++ b/module/apps/actor-flags.js
@@ -3,135 +3,137 @@
* @implements {DocumentSheet}
*/
export default class ActorSheetFlags extends DocumentSheet {
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- id: "actor-flags",
- classes: ["sw5e"],
- template: "systems/sw5e/templates/apps/actor-flags.html",
- width: 500,
- closeOnSubmit: true
- });
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- get title() {
- return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData() {
- const data = {};
- data.actor = this.object;
- data.classes = this._getClasses();
- data.flags = this._getFlags();
- data.bonuses = this._getBonuses();
- return data;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare an object of sorted classes.
- * @return {object}
- * @private
- */
- _getClasses() {
- const classes = this.object.items.filter(i => i.type === "class");
- return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
- obj[i.id] = i.name;
- return obj;
- }, {});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare an object of flags data which groups flags by section
- * Add some additional data for rendering
- * @return {object}
- * @private
- */
- _getFlags() {
- const flags = {};
- const baseData = this.document.toJSON();
- for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
- if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
- let flag = foundry.utils.deepClone(v);
- flag.type = v.type.name;
- flag.isCheckbox = v.type === Boolean;
- flag.isSelect = v.hasOwnProperty('choices');
- flag.value = getProperty(baseData.flags, `sw5e.${k}`);
- flags[v.section][`flags.sw5e.${k}`] = flag;
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: "actor-flags",
+ classes: ["sw5e"],
+ template: "systems/sw5e/templates/apps/actor-flags.html",
+ width: 500,
+ closeOnSubmit: true
+ });
}
- return flags;
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Get the bonuses fields and their localization strings
- * @return {Array}
- * @private
- */
- _getBonuses() {
- const bonuses = [
- {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"},
- {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
- {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
- {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
- {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
- {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
- {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
- {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
- {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
- {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
- {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
- {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"},
- {name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
- {name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
- {name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
- {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
- ];
- for ( let b of bonuses ) {
- b.value = getProperty(this.object._data, b.name) || "";
+ /** @override */
+ get title() {
+ return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
}
- return bonuses;
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- async _updateObject(event, formData) {
- const actor = this.object;
- let updateData = expandObject(formData);
+ /** @override */
+ getData() {
+ const data = {};
+ data.actor = this.object;
+ data.classes = this._getClasses();
+ data.flags = this._getFlags();
+ data.bonuses = this._getBonuses();
+ return data;
+ }
- // 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}`) ) {
- unset = true;
- flags[`-=${k}`] = null;
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare an object of sorted classes.
+ * @return {object}
+ * @private
+ */
+ _getClasses() {
+ const classes = this.object.items.filter((i) => i.type === "class");
+ return classes
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .reduce((obj, i) => {
+ obj[i.id] = i.name;
+ return obj;
+ }, {});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare an object of flags data which groups flags by section
+ * Add some additional data for rendering
+ * @return {object}
+ * @private
+ */
+ _getFlags() {
+ const flags = {};
+ const baseData = this.document.toJSON();
+ for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
+ if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
+ let flag = foundry.utils.deepClone(v);
+ flag.type = v.type.name;
+ flag.isCheckbox = v.type === Boolean;
+ flag.isSelect = v.hasOwnProperty("choices");
+ flag.value = getProperty(baseData.flags, `sw5e.${k}`);
+ flags[v.section][`flags.sw5e.${k}`] = flag;
}
- }
+ return flags;
}
- // Clear any bonuses which are whitespace only
- for ( let b of Object.values(updateData.data.bonuses ) ) {
- for ( let [k, v] of Object.entries(b) ) {
- b[k] = v.trim();
- }
+ /* -------------------------------------------- */
+
+ /**
+ * Get the bonuses fields and their localization strings
+ * @return {Array}
+ * @private
+ */
+ _getBonuses() {
+ const bonuses = [
+ {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"},
+ {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
+ {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
+ {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
+ {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
+ {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
+ {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
+ {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
+ {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
+ {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
+ {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
+ {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"},
+ {name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
+ {name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
+ {name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
+ {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
+ ];
+ for (let b of bonuses) {
+ b.value = getProperty(this.object._data, b.name) || "";
+ }
+ return bonuses;
}
- // Diff the data against any applied overrides and apply
- await actor.update(updateData, {diff: false});
- }
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _updateObject(event, formData) {
+ const actor = this.object;
+ let updateData = expandObject(formData);
+
+ // 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}`)) {
+ unset = true;
+ flags[`-=${k}`] = null;
+ }
+ }
+ }
+
+ // Clear any bonuses which are whitespace only
+ for (let b of Object.values(updateData.data.bonuses)) {
+ for (let [k, v] of Object.entries(b)) {
+ b[k] = v.trim();
+ }
+ }
+
+ // Diff the data against any applied overrides and apply
+ await actor.update(updateData, {diff: false});
+ }
}
diff --git a/module/apps/actor-type.js b/module/apps/actor-type.js
index ad56e72b..7ad1ffe9 100644
--- a/module/apps/actor-type.js
+++ b/module/apps/actor-type.js
@@ -5,7 +5,6 @@ import Actor5e from "../actor/entity.js";
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
-
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@@ -32,23 +31,23 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */
getData(options) {
-
// Get current value or new default
- let attr = foundry.utils.getProperty(this.object.data.data, 'details.type');
- if ( foundry.utils.getType(attr) !== "Object" ) attr = {
- value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid",
- subtype: "",
- swarm: "",
- custom: ""
- };
+ let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
+ if (foundry.utils.getType(attr) !== "Object")
+ attr = {
+ value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
+ subtype: "",
+ swarm: "",
+ custom: ""
+ };
// Populate choices
const types = {};
- for ( let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes) ) {
+ for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) {
types[k] = {
label: game.i18n.localize(v),
chosen: attr.value === k
- }
+ };
}
// Return data for rendering
@@ -61,12 +60,14 @@ export default class ActorTypeConfig extends FormApplication {
},
subtype: attr.subtype,
swarm: attr.swarm,
- sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => {
- obj[e[0]] = e[1];
- return obj;
- }, {}),
+ sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
+ .reverse()
+ .reduce((obj, e) => {
+ obj[e[0]] = e[1];
+ return obj;
+ }, {}),
preview: Actor5e.formatCreatureType(attr) || "–"
- }
+ };
}
/* -------------------------------------------- */
@@ -74,7 +75,7 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
- return this.object.update({ 'data.details.type': typeObject });
+ return this.object.update({"data.details.type": typeObject});
}
/* -------------------------------------------- */
diff --git a/module/apps/hit-dice-config.js b/module/apps/hit-dice-config.js
index d36d6bc2..f4fdf276 100644
--- a/module/apps/hit-dice-config.js
+++ b/module/apps/hit-dice-config.js
@@ -3,7 +3,6 @@
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
-
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@@ -26,20 +25,22 @@ export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
getData(options) {
return {
- classes: this.object.items.reduce((classes, item) => {
- if (item.data.type === "class") {
- // Add the appropriate data only if this item is a "class"
- classes.push({
- classItemId: item.data._id,
- name: item.data.name,
- diceDenom: item.data.data.hitDice,
- currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
- maxHitDice: item.data.data.levels,
- canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0
- });
- }
- return classes;
- }, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
+ classes: this.object.items
+ .reduce((classes, item) => {
+ if (item.data.type === "class") {
+ // Add the appropriate data only if this item is a "class"
+ classes.push({
+ classItemId: item.data._id,
+ name: item.data.name,
+ diceDenom: item.data.data.hitDice,
+ currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
+ maxHitDice: item.data.data.levels,
+ canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
+ });
+ }
+ return classes;
+ }, [])
+ .sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
};
}
@@ -50,7 +51,7 @@ export default class ActorHitDiceConfig extends DocumentSheet {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
- html.find("button.increment,button.decrement").click(event => {
+ html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
@@ -67,8 +68,8 @@ export default class ActorHitDiceConfig extends DocumentSheet {
async _updateObject(event, formData) {
const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
- _id: id,
- "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd,
+ "_id": id,
+ "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}
diff --git a/module/apps/long-rest.js b/module/apps/long-rest.js
index 3e57cea7..a34e53c7 100644
--- a/module/apps/long-rest.js
+++ b/module/apps/long-rest.js
@@ -3,65 +3,65 @@
* @extends {Dialog}
*/
export default class LongRestDialog extends Dialog {
- constructor(actor, dialogData = {}, options = {}) {
- super(dialogData, options);
- this.actor = actor;
- }
+ constructor(actor, dialogData = {}, options = {}) {
+ super(dialogData, options);
+ this.actor = actor;
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- template: "systems/sw5e/templates/apps/long-rest.html",
- classes: ["sw5e", "dialog"]
- });
- }
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ template: "systems/sw5e/templates/apps/long-rest.html",
+ classes: ["sw5e", "dialog"]
+ });
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- getData() {
- const data = super.getData();
- const variant = game.settings.get("sw5e", "restVariant");
- data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
- data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
- return data;
- }
+ /** @override */
+ getData() {
+ const data = super.getData();
+ const variant = game.settings.get("sw5e", "restVariant");
+ data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
+ data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
+ return data;
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
- * workflow has been resolved.
- * @param {Actor5e} actor
- * @return {Promise}
- */
- static async longRestDialog({ actor } = {}) {
- return new Promise((resolve, reject) => {
- const dlg = new this(actor, {
- title: game.i18n.localize("SW5E.LongRest"),
- buttons: {
- rest: {
- icon: ' ',
- label: game.i18n.localize("SW5E.Rest"),
- callback: html => {
- let newDay = true;
- if (game.settings.get("sw5e", "restVariant") !== "gritty")
- newDay = html.find('input[name="newDay"]')[0].checked;
- resolve(newDay);
- }
- },
- cancel: {
- icon: ' ',
- label: game.i18n.localize("Cancel"),
- callback: reject
- }
- },
- default: 'rest',
- close: reject
- });
- dlg.render(true);
- });
- }
-}
\ No newline at end of file
+ /**
+ * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
+ * workflow has been resolved.
+ * @param {Actor5e} actor
+ * @return {Promise}
+ */
+ static async longRestDialog({actor} = {}) {
+ return new Promise((resolve, reject) => {
+ const dlg = new this(actor, {
+ title: game.i18n.localize("SW5E.LongRest"),
+ buttons: {
+ rest: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.Rest"),
+ callback: (html) => {
+ let newDay = true;
+ if (game.settings.get("sw5e", "restVariant") !== "gritty")
+ newDay = html.find('input[name="newDay"]')[0].checked;
+ resolve(newDay);
+ }
+ },
+ cancel: {
+ icon: ' ',
+ label: game.i18n.localize("Cancel"),
+ callback: reject
+ }
+ },
+ default: "rest",
+ close: reject
+ });
+ dlg.render(true);
+ });
+ }
+}
diff --git a/module/apps/movement-config.js b/module/apps/movement-config.js
index c507965d..3c43170e 100644
--- a/module/apps/movement-config.js
+++ b/module/apps/movement-config.js
@@ -3,37 +3,36 @@
* @extends {DocumentSheet}
*/
export default class ActorMovementConfig extends DocumentSheet {
-
- /** @override */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ["sw5e"],
- template: "systems/sw5e/templates/apps/movement-config.html",
- width: 300,
- height: "auto"
- });
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- get title() {
- return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData(options) {
- const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
- const data = {
- movement: foundry.utils.deepClone(sourceMovement),
- 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;
+ /** @override */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ classes: ["sw5e"],
+ template: "systems/sw5e/templates/apps/movement-config.html",
+ width: 300,
+ height: "auto"
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ get title() {
+ return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData(options) {
+ const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
+ const data = {
+ movement: foundry.utils.deepClone(sourceMovement),
+ 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;
}
- return data;
- }
}
diff --git a/module/apps/select-items-prompt.js b/module/apps/select-items-prompt.js
index 0eac6497..cd99a278 100644
--- a/module/apps/select-items-prompt.js
+++ b/module/apps/select-items-prompt.js
@@ -3,7 +3,7 @@
* @type {Dialog}
*/
export default class SelectItemsPrompt extends Dialog {
- constructor(items, dialogData={}, options={}) {
+ constructor(items, dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
@@ -18,11 +18,11 @@ export default class SelectItemsPrompt extends Dialog {
super.activateListeners(html);
// render the item's sheet if its image is clicked
- html.on('click', '.item-image', (event) => {
+ html.on("click", ".item-image", (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true);
- })
+ });
}
/**
@@ -33,29 +33,27 @@ export default class SelectItemsPrompt extends Dialog {
* @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise} - list of item ids which the user has selected
*/
- static async create(items, {
- hint
- }) {
+ static async create(items, {hint}) {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
return new Promise((resolve) => {
const dlg = new this(items, {
- title: game.i18n.localize('SW5E.SelectItemsPromptTitle'),
+ title: game.i18n.localize("SW5E.SelectItemsPromptTitle"),
content: html,
buttons: {
apply: {
icon: ` `,
- label: game.i18n.localize('SW5E.Apply'),
- callback: html => {
+ label: game.i18n.localize("SW5E.Apply"),
+ callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
- const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]);
+ const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]);
resolve(selectedIds);
}
},
cancel: {
icon: ' ',
- label: game.i18n.localize('SW5E.Skip'),
+ label: game.i18n.localize("SW5E.Skip"),
callback: () => resolve([])
}
},
diff --git a/module/apps/senses-config.js b/module/apps/senses-config.js
index 707ca7fb..e12e5478 100644
--- a/module/apps/senses-config.js
+++ b/module/apps/senses-config.js
@@ -3,41 +3,41 @@
* @extends {DocumentSheet}
*/
export default class ActorSensesConfig extends DocumentSheet {
-
- /** @inheritdoc */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ["sw5e"],
- template: "systems/sw5e/templates/apps/senses-config.html",
- width: 300,
- height: "auto"
- });
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- get title() {
- return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- getData(options) {
- const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
- const data = {
- senses: {},
- special: senses.special ?? "",
- units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
- };
- for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
- const v = senses[name];
- data.senses[name] = {
- label: game.i18n.localize(label),
- value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
- }
+ /** @inheritdoc */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ classes: ["sw5e"],
+ template: "systems/sw5e/templates/apps/senses-config.html",
+ width: 300,
+ height: "auto"
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ get title() {
+ return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ getData(options) {
+ const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
+ const data = {
+ senses: {},
+ special: senses.special ?? "",
+ units: senses.units,
+ movementUnits: CONFIG.SW5E.movementUnits
+ };
+ for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) {
+ const v = senses[name];
+ data.senses[name] = {
+ label: game.i18n.localize(label),
+ value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
+ };
+ }
+ return data;
}
- return data;
- }
}
diff --git a/module/apps/short-rest.js b/module/apps/short-rest.js
index 22a186ab..95e7c68a 100644
--- a/module/apps/short-rest.js
+++ b/module/apps/short-rest.js
@@ -5,129 +5,130 @@ import LongRestDialog from "./long-rest.js";
* @extends {Dialog}
*/
export default class ShortRestDialog extends Dialog {
- constructor(actor, dialogData={}, options={}) {
- super(dialogData, options);
+ constructor(actor, dialogData = {}, options = {}) {
+ super(dialogData, options);
- /**
- * Store a reference to the Actor entity which is resting
- * @type {Actor}
- */
- this.actor = actor;
+ /**
+ * Store a reference to the Actor entity which is resting
+ * @type {Actor}
+ */
+ this.actor = actor;
- /**
- * Track the most recently used HD denomination for re-rendering the form
- * @type {string}
- */
- this._denom = null;
- }
+ /**
+ * Track the most recently used HD denomination for re-rendering the form
+ * @type {string}
+ */
+ this._denom = null;
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- template: "systems/sw5e/templates/apps/short-rest.html",
- classes: ["sw5e", "dialog"]
- });
- }
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ template: "systems/sw5e/templates/apps/short-rest.html",
+ classes: ["sw5e", "dialog"]
+ });
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @override */
- getData() {
- const data = super.getData();
+ /** @override */
+ getData() {
+ const data = super.getData();
- // Determine Hit Dice
- data.availableHD = this.actor.data.items.reduce((hd, item) => {
- if ( item.type === "class" ) {
- const d = item.data.data;
- const denom = d.hitDice || "d6";
- const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
- hd[denom] = denom in hd ? hd[denom] + available : available;
- }
- return hd;
- }, {});
- 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
- data.newDay = false; // It may be a new day, but not by default
- return data;
- }
-
- /* -------------------------------------------- */
-
-
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
- let btn = html.find("#roll-hd");
- btn.click(this._onRollHitDie.bind(this));
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle rolling a Hit Die as part of a Short Rest action
- * @param {Event} event The triggering click event
- * @private
- */
- async _onRollHitDie(event) {
- event.preventDefault();
- const btn = event.currentTarget;
- this._denom = btn.form.hd.value;
- await this.actor.rollHitDie(this._denom);
- this.render();
- }
-
- /* -------------------------------------------- */
-
- /**
- * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
- * been resolved.
- * @param {Actor5e} actor
- * @return {Promise}
- */
- static async shortRestDialog({actor}={}) {
- return new Promise((resolve, reject) => {
- const dlg = new this(actor, {
- title: game.i18n.localize("SW5E.ShortRest"),
- buttons: {
- rest: {
- icon: ' ',
- label: game.i18n.localize("SW5E.Rest"),
- callback: html => {
- let newDay = false;
- if (game.settings.get("sw5e", "restVariant") === "gritty")
- newDay = html.find('input[name="newDay"]')[0].checked;
- resolve(newDay);
+ // Determine Hit Dice
+ data.availableHD = this.actor.data.items.reduce((hd, item) => {
+ if (item.type === "class") {
+ const d = item.data.data;
+ const denom = d.hitDice || "d6";
+ const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
+ hd[denom] = denom in hd ? hd[denom] + available : available;
}
- },
- cancel: {
- icon: ' ',
- label: game.i18n.localize("Cancel"),
- callback: reject
- }
- },
- close: reject
- });
- dlg.render(true);
- });
- }
+ return hd;
+ }, {});
+ 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
+ data.newDay = false; // It may be a new day, but not by default
+ return data;
+ }
- /**
- * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
- * workflow has been resolved.
- * @deprecated
- * @param {Actor5e} actor
- * @return {Promise}
- */
- static async longRestDialog({actor}={}) {
- console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
- return LongRestDialog.longRestDialog(...arguments);
- }
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ let btn = html.find("#roll-hd");
+ btn.click(this._onRollHitDie.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling a Hit Die as part of a Short Rest action
+ * @param {Event} event The triggering click event
+ * @private
+ */
+ async _onRollHitDie(event) {
+ event.preventDefault();
+ const btn = event.currentTarget;
+ this._denom = btn.form.hd.value;
+ await this.actor.rollHitDie(this._denom);
+ this.render();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
+ * been resolved.
+ * @param {Actor5e} actor
+ * @return {Promise}
+ */
+ static async shortRestDialog({actor} = {}) {
+ return new Promise((resolve, reject) => {
+ const dlg = new this(actor, {
+ title: game.i18n.localize("SW5E.ShortRest"),
+ buttons: {
+ rest: {
+ icon: ' ',
+ label: game.i18n.localize("SW5E.Rest"),
+ callback: (html) => {
+ let newDay = false;
+ if (game.settings.get("sw5e", "restVariant") === "gritty")
+ newDay = html.find('input[name="newDay"]')[0].checked;
+ resolve(newDay);
+ }
+ },
+ cancel: {
+ icon: ' ',
+ label: game.i18n.localize("Cancel"),
+ callback: reject
+ }
+ },
+ close: reject
+ });
+ dlg.render(true);
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
+ * workflow has been resolved.
+ * @deprecated
+ * @param {Actor5e} actor
+ * @return {Promise}
+ */
+ static async longRestDialog({actor} = {}) {
+ console.warn(
+ "WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
+ );
+ return LongRestDialog.longRestDialog(...arguments);
+ }
}
diff --git a/module/apps/trait-selector.js b/module/apps/trait-selector.js
index ef3c82c1..6c454cf5 100644
--- a/module/apps/trait-selector.js
+++ b/module/apps/trait-selector.js
@@ -3,86 +3,85 @@
* @extends {DocumentSheet}
*/
export default class TraitSelector extends DocumentSheet {
-
- /** @inheritdoc */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- id: "trait-selector",
- classes: ["sw5e", "trait-selector", "subconfig"],
- title: "Actor Trait Selection",
- template: "systems/sw5e/templates/apps/trait-selector.html",
- width: 320,
- height: "auto",
- choices: {},
- allowCustom: true,
- minimum: 0,
- maximum: null,
- valueKey: "value",
- customKey: "custom"
- });
- }
-
- /* -------------------------------------------- */
-
- /**
- * Return a reference to the target attribute
- * @type {string}
- */
- get attribute() {
- return this.options.name;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData() {
- const attr = foundry.utils.getProperty(this.object.data, this.attribute);
- const o = this.options;
- const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr;
- const custom = (o.customKey) ? attr[o.customKey] ?? "" : "";
-
- // Populate choices
- const choices = Object.entries(o.choices).reduce((obj, e) => {
- let [k, v] = e;
- obj[k] = { label: v, chosen: attr ? value.includes(k) : false };
- return obj;
- }, {})
-
- // Return data
- return {
- allowCustom: o.allowCustom,
- choices: choices,
- custom: custom
- }
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async _updateObject(event, formData) {
- const o = this.options;
-
- // Obtain choices
- const chosen = [];
- for ( let [k, v] of Object.entries(formData) ) {
- if ( (k !== "custom") && v ) chosen.push(k);
+ /** @inheritdoc */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: "trait-selector",
+ classes: ["sw5e", "trait-selector", "subconfig"],
+ title: "Actor Trait Selection",
+ template: "systems/sw5e/templates/apps/trait-selector.html",
+ width: 320,
+ height: "auto",
+ choices: {},
+ allowCustom: true,
+ minimum: 0,
+ maximum: null,
+ valueKey: "value",
+ customKey: "custom"
+ });
}
- // Object including custom data
- const updateData = {};
- if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
- else updateData[this.attribute] = chosen;
- if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
+ /* -------------------------------------------- */
- // Validate the number chosen
- if ( o.minimum && (chosen.length < o.minimum) ) {
- return ui.notifications.error(`You must choose at least ${o.minimum} options`);
- }
- if ( o.maximum && (chosen.length > o.maximum) ) {
- return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
+ /**
+ * Return a reference to the target attribute
+ * @type {string}
+ */
+ get attribute() {
+ return this.options.name;
}
- // Update the object
- this.object.update(updateData);
- }
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData() {
+ const attr = foundry.utils.getProperty(this.object.data, this.attribute);
+ const o = this.options;
+ const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
+ const custom = o.customKey ? attr[o.customKey] ?? "" : "";
+
+ // Populate choices
+ const choices = Object.entries(o.choices).reduce((obj, e) => {
+ let [k, v] = e;
+ obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
+ return obj;
+ }, {});
+
+ // Return data
+ return {
+ allowCustom: o.allowCustom,
+ choices: choices,
+ custom: custom
+ };
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _updateObject(event, formData) {
+ const o = this.options;
+
+ // Obtain choices
+ const chosen = [];
+ for (let [k, v] of Object.entries(formData)) {
+ if (k !== "custom" && v) chosen.push(k);
+ }
+
+ // Object including custom data
+ const updateData = {};
+ if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
+ else updateData[this.attribute] = chosen;
+ if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
+
+ // Validate the number chosen
+ if (o.minimum && chosen.length < o.minimum) {
+ return ui.notifications.error(`You must choose at least ${o.minimum} options`);
+ }
+ if (o.maximum && chosen.length > o.maximum) {
+ return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
+ }
+
+ // Update the object
+ this.object.update(updateData);
+ }
}
diff --git a/module/canvas.js b/module/canvas.js
index 622e2a30..72c60c81 100644
--- a/module/canvas.js
+++ b/module/canvas.js
@@ -1,38 +1,38 @@
/** @override */
-export const measureDistances = function(segments, options={}) {
- if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
+export const measureDistances = function (segments, options = {}) {
+ if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
- // Track the total number of diagonals
- let nDiagonal = 0;
- const rule = this.parent.diagonalRule;
- const d = canvas.dimensions;
+ // Track the total number of diagonals
+ let nDiagonal = 0;
+ const rule = this.parent.diagonalRule;
+ const d = canvas.dimensions;
- // Iterate over measured segments
- return segments.map(s => {
- let r = s.ray;
+ // Iterate over measured segments
+ return segments.map((s) => {
+ let r = s.ray;
- // Determine the total distance traveled
- let nx = Math.abs(Math.ceil(r.dx / d.size));
- let ny = Math.abs(Math.ceil(r.dy / d.size));
+ // Determine the total distance traveled
+ let nx = Math.abs(Math.ceil(r.dx / d.size));
+ let ny = Math.abs(Math.ceil(r.dy / d.size));
- // Determine the number of straight and diagonal moves
- let nd = Math.min(nx, ny);
- let ns = Math.abs(ny - nx);
- nDiagonal += nd;
+ // Determine the number of straight and diagonal moves
+ let nd = Math.min(nx, ny);
+ let ns = Math.abs(ny - nx);
+ nDiagonal += nd;
- // Alternative DMG Movement
- if (rule === "5105") {
- let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
- let spaces = (nd10 * 2) + (nd - nd10) + ns;
- return spaces * canvas.dimensions.distance;
- }
+ // Alternative DMG Movement
+ if (rule === "5105") {
+ let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
+ let spaces = nd10 * 2 + (nd - nd10) + ns;
+ return spaces * canvas.dimensions.distance;
+ }
- // Euclidean Measurement
- else if (rule === "EUCL") {
- return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
- }
+ // Euclidean Measurement
+ else if (rule === "EUCL") {
+ return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
+ }
- // Standard PHB Movement
- else return (ns + nd) * canvas.scene.data.gridDistance;
- });
-};
\ No newline at end of file
+ // Standard PHB Movement
+ else return (ns + nd) * canvas.scene.data.gridDistance;
+ });
+};
diff --git a/module/characterImporter.js b/module/characterImporter.js
index 8461976a..178be216 100644
--- a/module/characterImporter.js
+++ b/module/characterImporter.js
@@ -1,51 +1,51 @@
export default class CharacterImporter {
- // transform JSON from sw5e.com to Foundry friendly format
- // and insert new actor
- static async transform(rawCharacter) {
- const sourceCharacter = JSON.parse(rawCharacter); //source character
+ // transform JSON from sw5e.com to Foundry friendly format
+ // and insert new actor
+ static async transform(rawCharacter) {
+ const sourceCharacter = JSON.parse(rawCharacter); //source character
- const details = {
- species: sourceCharacter.attribs.find((e) => e.name == "race").current,
- background: sourceCharacter.attribs.find((e) => e.name == "background").current,
- alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
- };
+ const details = {
+ species: sourceCharacter.attribs.find((e) => e.name == "race").current,
+ background: sourceCharacter.attribs.find((e) => e.name == "background").current,
+ alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
+ };
- const hp = {
- value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
- min: 0,
- max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
- temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
- };
+ const hp = {
+ value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
+ min: 0,
+ max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
+ temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
+ };
- const abilities = {
- str: {
- value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
- proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
- },
- dex: {
- value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
- proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
- },
- con: {
- value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
- proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
- },
- int: {
- value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
- proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
- },
- wis: {
- value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
- proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
- },
- cha: {
- value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
- proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
- }
- };
+ const abilities = {
+ str: {
+ value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
+ proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
+ },
+ dex: {
+ value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
+ proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
+ },
+ con: {
+ value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
+ proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
+ },
+ int: {
+ value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
+ proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
+ },
+ wis: {
+ value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
+ proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
+ },
+ cha: {
+ value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
+ proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
+ }
+ };
- /* ----------------------------------------------------------------- */
- /* character.data.skills..value is all that matters
+ /* ----------------------------------------------------------------- */
+ /* character.data.skills..value is all that matters
/* values can be 0, 0.5, 1 or 2
/* 0 = regular
/* 0.5 = half-proficient
@@ -53,272 +53,274 @@ export default class CharacterImporter {
/* 2 = expertise
/* foundry takes care of calculating the rest
/* ----------------------------------------------------------------- */
- const skills = {
- acr: {
- value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
- },
- ani: {
- value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
- },
- ath: {
- value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
- },
- dec: {
- value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
- },
- ins: {
- value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
- },
- inv: {
- value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
- },
- itm: {
- value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
- },
- lor: {
- value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
- },
- med: {
- value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
- },
- nat: {
- value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
- },
- per: {
- value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
- },
- pil: {
- value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
- },
- prc: {
- value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
- },
- prf: {
- value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
- },
- slt: {
- value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
- },
- ste: {
- value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
- },
- sur: {
- value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
- },
- tec: {
- value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
- }
- };
-
- const targetCharacter = {
- name: sourceCharacter.name,
- type: "character",
- data: {
- abilities: abilities,
- details: details,
- skills: skills,
- attributes: {
- hp: hp
- }
- }
- };
-
- let actor = await Actor.create(targetCharacter);
- CharacterImporter.addProfessions(sourceCharacter, actor);
- }
-
- // Parse all classes and add them to already created actor.
- // "class" is a reserved word, therefore I use profession where I can.
- static async addProfessions(sourceCharacter, actor) {
- let result = [];
-
- // parse all class and multiclassX items
- // couldn't get Array.filter to work here for some reason
- // result = array of objects. each object is a separate class
- sourceCharacter.attribs.forEach((e) => {
- if (CharacterImporter.classOrMulticlass(e.name)) {
- var t = {
- profession: CharacterImporter.capitalize(e.current),
- type: CharacterImporter.baseOrMulti(e.name),
- level: CharacterImporter.getLevel(e, sourceCharacter)
+ const skills = {
+ acr: {
+ value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
+ },
+ ani: {
+ value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
+ },
+ ath: {
+ value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
+ },
+ dec: {
+ value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
+ },
+ ins: {
+ value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
+ },
+ inv: {
+ value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
+ },
+ itm: {
+ value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
+ },
+ lor: {
+ value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
+ },
+ med: {
+ value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
+ },
+ nat: {
+ value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
+ },
+ per: {
+ value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
+ },
+ pil: {
+ value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
+ },
+ prc: {
+ value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
+ },
+ prf: {
+ value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
+ },
+ slt: {
+ value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
+ },
+ ste: {
+ value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
+ },
+ sur: {
+ value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
+ },
+ tec: {
+ value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
+ }
};
- result.push(t);
- }
- });
- // pull classes directly from system compendium and add them to current actor
- const professionsPack = await game.packs.get("sw5e.classes").getContent();
- result.forEach((prof) => {
- let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
- assignedProfession.data.data.levels = prof.level;
- actor.createEmbeddedEntity("OwnedItem", assignedProfession.data, { displaySheet: false });
- });
+ const targetCharacter = {
+ name: sourceCharacter.name,
+ type: "character",
+ data: {
+ abilities: abilities,
+ details: details,
+ skills: skills,
+ attributes: {
+ hp: hp
+ }
+ }
+ };
- this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
-
- this.addPowers(
- sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current),
- actor
- );
-
- const discoveredItems = sourceCharacter.attribs.filter(
- (e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
- );
- const items = discoveredItems.map((item) => {
- const id = item.name.match(/-\w{19}/g);
-
- return {
- name: item.current,
- quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
- };
- });
-
- this.addItems(items, actor);
- }
-
- static async addClasses(profession, level, actor) {
- let classes = await game.packs.get("sw5e.classes").getContent();
- let assignedClass = classes.find((c) => c.name === profession);
- assignedClass.data.data.levels = level;
- await actor.createEmbeddedEntity("OwnedItem", assignedClass.data, { displaySheet: false });
- }
-
- static classOrMulticlass(name) {
- return name === "class" || (name.includes("multiclass") && name.length <= 12);
- }
-
- static baseOrMulti(name) {
- if (name === "class") {
- return "base_class";
- } else {
- return "multi_class";
+ let actor = await Actor.create(targetCharacter);
+ CharacterImporter.addProfessions(sourceCharacter, actor);
}
- }
- static getLevel(item, sourceCharacter) {
- if (item.name === "class") {
- let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
- return parseInt(result);
- } else {
- let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
- return parseInt(result);
+ // Parse all classes and add them to already created actor.
+ // "class" is a reserved word, therefore I use profession where I can.
+ static async addProfessions(sourceCharacter, actor) {
+ let result = [];
+
+ // parse all class and multiclassX items
+ // couldn't get Array.filter to work here for some reason
+ // result = array of objects. each object is a separate class
+ sourceCharacter.attribs.forEach((e) => {
+ if (CharacterImporter.classOrMulticlass(e.name)) {
+ var t = {
+ profession: CharacterImporter.capitalize(e.current),
+ type: CharacterImporter.baseOrMulti(e.name),
+ level: CharacterImporter.getLevel(e, sourceCharacter)
+ };
+ result.push(t);
+ }
+ });
+
+ // pull classes directly from system compendium and add them to current actor
+ const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
+ result.forEach((prof) => {
+ let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
+ assignedProfession.data.data.levels = prof.level;
+ actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
+ });
+
+ this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
+
+ this.addPowers(
+ sourceCharacter.attribs
+ .filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
+ .map((e) => e.current),
+ actor
+ );
+
+ const discoveredItems = sourceCharacter.attribs.filter(
+ (e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
+ );
+ const items = discoveredItems.map((item) => {
+ const id = item.name.match(/-\w{19}/g);
+
+ return {
+ name: item.current,
+ quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
+ };
+ });
+
+ this.addItems(items, actor);
}
- }
- static capitalize(str) {
- return str.charAt(0).toUpperCase() + str.slice(1);
- }
-
- static async addSpecies(race, actor) {
- const species = await game.packs.get("sw5e.species").getContent();
- const assignedSpecies = species.find((c) => c.name === race);
- const activeEffects = assignedSpecies.data.effects[0].changes;
- const actorData = { data: { abilities: { ...actor.data.data.abilities } } };
-
- activeEffects.map((effect) => {
- switch (effect.key) {
- case "data.abilities.str.value":
- actorData.data.abilities.str.value -= effect.value;
- break;
-
- case "data.abilities.dex.value":
- actorData.data.abilities.dex.value -= effect.value;
- break;
-
- case "data.abilities.con.value":
- actorData.data.abilities.con.value -= effect.value;
- break;
-
- case "data.abilities.int.value":
- actorData.data.abilities.int.value -= effect.value;
- break;
-
- case "data.abilities.wis.value":
- actorData.data.abilities.wis.value -= effect.value;
- break;
-
- case "data.abilities.cha.value":
- actorData.data.abilities.cha.value -= effect.value;
- break;
-
- default:
- break;
- }
- });
-
- actor.update(actorData);
-
- await actor.createEmbeddedEntity("OwnedItem", assignedSpecies.data, { displaySheet: false });
- }
-
- static async addPowers(powers, actor) {
- const forcePowers = await game.packs.get("sw5e.forcepowers").getContent();
- const techPowers = await game.packs.get("sw5e.techpowers").getContent();
-
- for (const power of powers) {
- const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
-
- if (createdPower) {
- await actor.createEmbeddedEntity("OwnedItem", createdPower.data, { displaySheet: false });
- }
+ static async addClasses(profession, level, actor) {
+ let classes = await game.packs.get("sw5e.classes").getDocuments();
+ let assignedClass = classes.find((c) => c.name === profession);
+ assignedClass.data.data.levels = level;
+ await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false});
}
- }
- static async addItems(items, actor) {
- const weapons = await game.packs.get("sw5e.weapons").getContent();
- const armors = await game.packs.get("sw5e.armor").getContent();
- const adventuringGear = await game.packs.get("sw5e.adventuringgear").getContent();
+ static classOrMulticlass(name) {
+ return name === "class" || (name.includes("multiclass") && name.length <= 12);
+ }
- for (const item of items) {
- const createdItem =
- weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
- armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
- adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
-
- if (createdItem) {
- if (item.quantity != 1) {
- createdItem.data.data.quantity = item.quantity;
+ static baseOrMulti(name) {
+ if (name === "class") {
+ return "base_class";
+ } else {
+ return "multi_class";
}
-
- await actor.createEmbeddedEntity("OwnedItem", createdItem.data, { displaySheet: false });
- }
}
- }
- static addImportButton(html) {
- const actionButtons = html.find(".header-actions");
- actionButtons[0].insertAdjacentHTML(
- "afterend",
- ``
- );
+ static getLevel(item, sourceCharacter) {
+ if (item.name === "class") {
+ let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
+ return parseInt(result);
+ } else {
+ let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
+ return parseInt(result);
+ }
+ }
- let characterImportButton = $(".cs-import-button");
- characterImportButton.click(() => {
- let content = `Saved Character JSON Import
+ static capitalize(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+ }
+
+ static async addSpecies(race, actor) {
+ const species = await game.packs.get("sw5e.species").getDocuments();
+ const assignedSpecies = species.find((c) => c.name === race);
+ const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
+ const actorData = {data: {abilities: {...actor.data.data.abilities}}};
+
+ activeEffects.map((effect) => {
+ switch (effect.key) {
+ case "data.abilities.str.value":
+ actorData.data.abilities.str.value -= effect.value;
+ break;
+
+ case "data.abilities.dex.value":
+ actorData.data.abilities.dex.value -= effect.value;
+ break;
+
+ case "data.abilities.con.value":
+ actorData.data.abilities.con.value -= effect.value;
+ break;
+
+ case "data.abilities.int.value":
+ actorData.data.abilities.int.value -= effect.value;
+ break;
+
+ case "data.abilities.wis.value":
+ actorData.data.abilities.wis.value -= effect.value;
+ break;
+
+ case "data.abilities.cha.value":
+ actorData.data.abilities.cha.value -= effect.value;
+ break;
+
+ default:
+ break;
+ }
+ });
+
+ actor.update(actorData);
+
+ await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
+ }
+
+ static async addPowers(powers, actor) {
+ const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
+ const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
+
+ for (const power of powers) {
+ const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
+
+ if (createdPower) {
+ await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
+ }
+ }
+ }
+
+ static async addItems(items, actor) {
+ const weapons = await game.packs.get("sw5e.weapons").getDocuments();
+ const armors = await game.packs.get("sw5e.armor").getDocuments();
+ const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
+
+ for (const item of items) {
+ const createdItem =
+ weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
+ armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
+ adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
+
+ if (createdItem) {
+ if (item.quantity != 1) {
+ createdItem.data.data.quantity = item.quantity;
+ }
+
+ await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
+ }
+ }
+ }
+
+ static addImportButton(html) {
+ const actionButtons = html.find(".header-actions");
+ actionButtons[0].insertAdjacentHTML(
+ "afterend",
+ ``
+ );
+
+ let characterImportButton = $(".cs-import-button");
+ characterImportButton.click(() => {
+ let content = `Saved Character JSON Import
Paste character JSON here:
`;
- let importDialog = new Dialog({
- title: "Import Character from SW5e.com",
- content: content,
- buttons: {
- Import: {
- icon: ` `,
- label: "Import Character",
- callback: () => {
- let characterData = $("#character-json").val();
- console.log("Parsing Character JSON");
- CharacterImporter.transform(characterData);
- }
- },
- Cancel: {
- icon: ` `,
- label: "Cancel",
- callback: () => {}
- }
- }
- });
- importDialog.render(true);
- });
- }
+ let importDialog = new Dialog({
+ title: "Import Character from SW5e.com",
+ content: content,
+ buttons: {
+ Import: {
+ icon: ` `,
+ label: "Import Character",
+ callback: () => {
+ let characterData = $("#character-json").val();
+ console.log("Parsing Character JSON");
+ CharacterImporter.transform(characterData);
+ }
+ },
+ Cancel: {
+ icon: ` `,
+ label: "Cancel",
+ callback: () => {}
+ }
+ }
+ });
+ importDialog.render(true);
+ });
+ }
}
diff --git a/module/chat.js b/module/chat.js
index d024d8aa..42d2bf4f 100644
--- a/module/chat.js
+++ b/module/chat.js
@@ -1,30 +1,29 @@
-
/**
* Highlight critical success or failure on d20 rolls
*/
-export const highlightCriticalSuccessFailure = function(message, html, data) {
- if ( !message.isRoll || !message.isContentVisible ) return;
+export const highlightCriticalSuccessFailure = function (message, html, data) {
+ if (!message.isRoll || !message.isContentVisible) return;
- // Highlight rolls where the first part is a d20 roll
- const roll = message.roll;
- if ( !roll.dice.length ) return;
- const d = roll.dice[0];
+ // Highlight rolls where the first part is a d20 roll
+ const roll = message.roll;
+ if (!roll.dice.length) return;
+ const d = roll.dice[0];
- // Ensure it is an un-modified d20 roll
- 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;
+ // Ensure it is an un-modified d20 roll
+ 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
- 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");
- }
+ // Highlight successes and failures
+ 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");
+ }
};
/* -------------------------------------------- */
@@ -32,24 +31,24 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
/**
* Optionally hide the display of chat card action buttons which cannot be performed by the user
*/
-export const displayChatActionButtons = function(message, html, data) {
- const chatCard = html.find(".sw5e.chat-card");
- if ( chatCard.length > 0 ) {
- const flavor = html.find(".flavor-text");
- if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
+export const displayChatActionButtons = function (message, html, data) {
+ const chatCard = html.find(".sw5e.chat-card");
+ if (chatCard.length > 0) {
+ 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);
- if ( actor && actor.isOwner ) return;
- else if ( game.user.isGM || (data.author.id === game.user.id)) return;
+ // If the user is the message author or the actor owner, proceed
+ let actor = game.actors.get(data.message.speaker.actor);
+ if (actor && actor.isOwner) return;
+ else if (game.user.isGM || data.author.id === game.user.id) return;
- // Otherwise conceal action buttons except for saving throw
- const buttons = chatCard.find("button[data-action]");
- buttons.each((i, btn) => {
- if ( btn.dataset.action === "save" ) return;
- btn.style.display = "none"
- });
- }
+ // Otherwise conceal action buttons except for saving throw
+ const buttons = chatCard.find("button[data-action]");
+ buttons.each((i, btn) => {
+ if (btn.dataset.action === "save") return;
+ btn.style.display = "none";
+ });
+ }
};
/* -------------------------------------------- */
@@ -63,38 +62,38 @@ export const displayChatActionButtons = function(message, html, data) {
*
* @return {Array} The extended options Array including new context choices
*/
-export const addChatMessageContextOptions = function(html, options) {
- let canApply = li => {
- const message = game.messages.get(li.data("messageId"));
- return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
- };
- options.push(
- {
- name: game.i18n.localize("SW5E.ChatContextDamage"),
- icon: ' ',
- condition: canApply,
- callback: li => applyChatCardDamage(li, 1)
- },
- {
- name: game.i18n.localize("SW5E.ChatContextHealing"),
- icon: ' ',
- condition: canApply,
- callback: li => applyChatCardDamage(li, -1)
- },
- {
- name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
- icon: ' ',
- condition: canApply,
- callback: li => applyChatCardDamage(li, 2)
- },
- {
- name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
- icon: ' ',
- condition: canApply,
- callback: li => applyChatCardDamage(li, 0.5)
- }
- );
- return options;
+export const addChatMessageContextOptions = function (html, options) {
+ let canApply = (li) => {
+ const message = game.messages.get(li.data("messageId"));
+ return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
+ };
+ options.push(
+ {
+ name: game.i18n.localize("SW5E.ChatContextDamage"),
+ icon: ' ',
+ condition: canApply,
+ callback: (li) => applyChatCardDamage(li, 1)
+ },
+ {
+ name: game.i18n.localize("SW5E.ChatContextHealing"),
+ icon: ' ',
+ condition: canApply,
+ callback: (li) => applyChatCardDamage(li, -1)
+ },
+ {
+ name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
+ icon: ' ',
+ condition: canApply,
+ callback: (li) => applyChatCardDamage(li, 2)
+ },
+ {
+ name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
+ icon: ' ',
+ condition: canApply,
+ callback: (li) => applyChatCardDamage(li, 0.5)
+ }
+ );
+ return options;
};
/* -------------------------------------------- */
@@ -108,12 +107,14 @@ export const addChatMessageContextOptions = function(html, options) {
* @return {Promise}
*/
function applyChatCardDamage(li, multiplier) {
- const message = game.messages.get(li.data("messageId"));
- const roll = message.roll;
- return Promise.all(canvas.tokens.controlled.map(t => {
- const a = t.actor;
- return a.applyDamage(roll.total, multiplier);
- }));
+ const message = game.messages.get(li.data("messageId"));
+ const roll = message.roll;
+ return Promise.all(
+ canvas.tokens.controlled.map((t) => {
+ const a = t.actor;
+ return a.applyDamage(roll.total, multiplier);
+ })
+ );
}
/* -------------------------------------------- */
diff --git a/module/classFeatures.js b/module/classFeatures.js
index 7946c252..17155d10 100644
--- a/module/classFeatures.js
+++ b/module/classFeatures.js
@@ -1,4 +1 @@
-export const ClassFeatures = {
-
-};
-
+export const ClassFeatures = {};
diff --git a/module/combat.js b/module/combat.js
index 58535411..ff7594a5 100644
--- a/module/combat.js
+++ b/module/combat.js
@@ -1,27 +1,31 @@
-
/**
* Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate
* Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail.
*/
-export const _getInitiativeFormula = function() {
- const actor = this.actor;
- if ( !actor ) return "1d20";
- const init = actor.data.data.attributes.init;
+export const _getInitiativeFormula = function () {
+ const actor = this.actor;
+ if (!actor) return "1d20";
+ const init = actor.data.data.attributes.init;
- // Construct initiative formula parts
- let nd = 1;
- let mods = "";
- if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=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];
+ // Construct initiative formula parts
+ let nd = 1;
+ let mods = "";
+ if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=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");
- if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
- return parts.filter(p => p !== null).join(" + ");
+ // Optionally apply Dexterity tiebreaker
+ const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
+ if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
+ return parts.filter((p) => p !== null).join(" + ");
};
diff --git a/module/config.js b/module/config.js
index d0991cb5..1770bcea 100644
--- a/module/config.js
+++ b/module/config.js
@@ -1,4 +1,4 @@
-import {ClassFeatures} from "./classFeatures.js"
+import {ClassFeatures} from "./classFeatures.js";
// Namespace SW5e Configuration Values
export const SW5E = {};
@@ -12,27 +12,26 @@ SW5E.ASCII = `
\\______ / \\__/\\ //______ /\\__ >
\\/ \\/ \\/ \\/ `;
-
/**
* The set of Ability Scores used within the system
* @type {Object}
*/
SW5E.abilities = {
- "str": "SW5E.AbilityStr",
- "dex": "SW5E.AbilityDex",
- "con": "SW5E.AbilityCon",
- "int": "SW5E.AbilityInt",
- "wis": "SW5E.AbilityWis",
- "cha": "SW5E.AbilityCha"
+ str: "SW5E.AbilityStr",
+ dex: "SW5E.AbilityDex",
+ con: "SW5E.AbilityCon",
+ int: "SW5E.AbilityInt",
+ wis: "SW5E.AbilityWis",
+ cha: "SW5E.AbilityCha"
};
SW5E.abilityAbbreviations = {
- "str": "SW5E.AbilityStrAbbr",
- "dex": "SW5E.AbilityDexAbbr",
- "con": "SW5E.AbilityConAbbr",
- "int": "SW5E.AbilityIntAbbr",
- "wis": "SW5E.AbilityWisAbbr",
- "cha": "SW5E.AbilityChaAbbr"
+ str: "SW5E.AbilityStrAbbr",
+ dex: "SW5E.AbilityDexAbbr",
+ con: "SW5E.AbilityConAbbr",
+ int: "SW5E.AbilityIntAbbr",
+ wis: "SW5E.AbilityWisAbbr",
+ cha: "SW5E.AbilityChaAbbr"
};
/* -------------------------------------------- */
@@ -42,15 +41,15 @@ SW5E.abilityAbbreviations = {
* @type {Object}
*/
SW5E.alignments = {
- 'll': "SW5E.AlignmentLL",
- 'nl': "SW5E.AlignmentNL",
- 'cl': "SW5E.AlignmentCL",
- 'lb': "SW5E.AlignmentLB",
- 'bn': "SW5E.AlignmentBN",
- 'cb': "SW5E.AlignmentCB",
- 'ld': "SW5E.AlignmentLD",
- 'nd': "SW5E.AlignmentND",
- 'cd': "SW5E.AlignmentCD"
+ ll: "SW5E.AlignmentLL",
+ nl: "SW5E.AlignmentNL",
+ cl: "SW5E.AlignmentCL",
+ lb: "SW5E.AlignmentLB",
+ bn: "SW5E.AlignmentBN",
+ cb: "SW5E.AlignmentCB",
+ ld: "SW5E.AlignmentLD",
+ nd: "SW5E.AlignmentND",
+ cd: "SW5E.AlignmentCD"
};
/* -------------------------------------------- */
@@ -60,9 +59,9 @@ SW5E.alignments = {
* @enum {number}
*/
SW5E.attunementTypes = {
- NONE: 0,
- REQUIRED: 1,
- ATTUNED: 2,
+ NONE: 0,
+ REQUIRED: 1,
+ ATTUNED: 2
};
/**
@@ -70,37 +69,37 @@ SW5E.attunementTypes = {
* @type {{"0": string, "1": string, "2": string}}
*/
SW5E.attunements = {
- 0: "SW5E.AttunementNone",
- 1: "SW5E.AttunementRequired",
- 2: "SW5E.AttunementAttuned"
+ 0: "SW5E.AttunementNone",
+ 1: "SW5E.AttunementRequired",
+ 2: "SW5E.AttunementAttuned"
};
/* -------------------------------------------- */
SW5E.weaponProficiencies = {
- "blp": "SW5E.WeaponBlasterPistolProficiency",
- "chk": "SW5E.WeaponChakramProficiency",
- "dbb": "SW5E.WeaponDoubleBladeProficiency",
- "dbs": "SW5E.WeaponDoubleSaberProficiency",
- "dsh": "SW5E.WeaponDoubleShotoProficiency",
- "dsw": "SW5E.WeaponDoubleSwordProficiency",
- "hid": "SW5E.WeaponHiddenBladeProficiency",
- "imp": "SW5E.WeaponImprovisedProficiency",
- "lfl": "SW5E.WeaponLightFoilProficiency",
- "lrg": "SW5E.WeaponLightRingProficiency",
- "mar": "SW5E.WeaponMartialProficiency",
- "mrb": "SW5E.WeaponMartialBlasterProficiency",
- "mlw": "SW5E.WeaponMartialLightweaponProficiency",
- "mvb": "SW5E.WeaponMartialVibroweaponProficiency",
- "ntl": "SW5E.WeaponNaturalProficiency",
- "swh": "SW5E.WeaponSaberWhipProficiency",
- "sim": "SW5E.WeaponSimpleProficiency",
- "smb": "SW5E.WeaponSimpleBlasterProficiency",
- "slw": "SW5E.WeaponSimpleLightweaponProficiency",
- "svb": "SW5E.WeaponSimpleVibroweaponProficiency",
- "tch": "SW5E.WeaponTechbladeProficiency",
- "vbr": "SW5E.WeaponVibrorapierProficiency",
- "vbw": "SW5E.WeaponVibrowhipProficiency"
+ blp: "SW5E.WeaponBlasterPistolProficiency",
+ chk: "SW5E.WeaponChakramProficiency",
+ dbb: "SW5E.WeaponDoubleBladeProficiency",
+ dbs: "SW5E.WeaponDoubleSaberProficiency",
+ dsh: "SW5E.WeaponDoubleShotoProficiency",
+ dsw: "SW5E.WeaponDoubleSwordProficiency",
+ hid: "SW5E.WeaponHiddenBladeProficiency",
+ imp: "SW5E.WeaponImprovisedProficiency",
+ lfl: "SW5E.WeaponLightFoilProficiency",
+ lrg: "SW5E.WeaponLightRingProficiency",
+ mar: "SW5E.WeaponMartialProficiency",
+ mrb: "SW5E.WeaponMartialBlasterProficiency",
+ mlw: "SW5E.WeaponMartialLightweaponProficiency",
+ mvb: "SW5E.WeaponMartialVibroweaponProficiency",
+ ntl: "SW5E.WeaponNaturalProficiency",
+ swh: "SW5E.WeaponSaberWhipProficiency",
+ sim: "SW5E.WeaponSimpleProficiency",
+ smb: "SW5E.WeaponSimpleBlasterProficiency",
+ slw: "SW5E.WeaponSimpleLightweaponProficiency",
+ svb: "SW5E.WeaponSimpleVibroweaponProficiency",
+ tch: "SW5E.WeaponTechbladeProficiency",
+ vbr: "SW5E.WeaponVibrorapierProficiency",
+ vbw: "SW5E.WeaponVibrowhipProficiency"
};
/**
@@ -108,14 +107,14 @@ SW5E.weaponProficiencies = {
* Used when a new player owned item is created
* @type {Object}
*/
- SW5E.weaponProficienciesMap = {
- "natural": true,
- "simpleVW": "sim",
- "simpleB": "sim",
- "simpleLW": "sim",
- "martialVW": "mar",
- "martialB": "mar",
- "martialLW": "mar"
+SW5E.weaponProficienciesMap = {
+ natural: true,
+ simpleVW: "sim",
+ simpleB: "sim",
+ simpleLW: "sim",
+ martialVW: "mar",
+ martialB: "mar",
+ martialLW: "mar"
};
// TODO: Check to see if this can be used
@@ -172,36 +171,36 @@ SW5E.weaponIds = {
/* -------------------------------------------- */
SW5E.toolProficiencies = {
- "armor": "SW5E.ToolArmormech",
- "arms": "SW5E.ToolArmstech",
- "arti": "SW5E.ToolArtificer",
- "art": "SW5E.ToolArtist",
- "astro": "SW5E.ToolAstrotech",
- "bio": "SW5E.ToolBiotech",
- "con": "SW5E.ToolConstructor",
- "cyb": "SW5E.ToolCybertech",
- "jew": "SW5E.ToolJeweler",
- "sur": "SW5E.ToolSurveyor",
- "syn": "SW5E.ToolSynthweaver",
- "tin": "SW5E.ToolTinker",
- "ant": "SW5E.ToolAntitoxkit",
- "arc": "SW5E.ToolArchaeologistKit",
- "aud": "SW5E.ToolAudiotechKit",
- "bioa": "SW5E.ToolBioanalysisKit",
- "brew": "SW5E.ToolBrewerKit",
- "chef": "SW5E.ToolChefKit",
- "demo": "SW5E.ToolDemolitionKit",
- "disg": "SW5E.ToolDisguiseKit",
- "forg": "SW5E.ToolForgeryKit",
- "mech": "SW5E.ToolMechanicKit",
- "game": "SW5E.ToolGamingSet",
- "poi": "SW5E.ToolPoisonKit",
- "scav": "SW5E.ToolScavengingKit",
- "secur": "SW5E.ToolSecurityKit",
- "slic": "SW5E.ToolSlicerKit",
- "spice": "SW5E.ToolSpiceKit",
- "music": "SW5E.ToolMusicalInstrument",
- "vehicle": "SW5E.ToolVehicle"
+ armor: "SW5E.ToolArmormech",
+ arms: "SW5E.ToolArmstech",
+ arti: "SW5E.ToolArtificer",
+ art: "SW5E.ToolArtist",
+ astro: "SW5E.ToolAstrotech",
+ bio: "SW5E.ToolBiotech",
+ con: "SW5E.ToolConstructor",
+ cyb: "SW5E.ToolCybertech",
+ jew: "SW5E.ToolJeweler",
+ sur: "SW5E.ToolSurveyor",
+ syn: "SW5E.ToolSynthweaver",
+ tin: "SW5E.ToolTinker",
+ ant: "SW5E.ToolAntitoxkit",
+ arc: "SW5E.ToolArchaeologistKit",
+ aud: "SW5E.ToolAudiotechKit",
+ bioa: "SW5E.ToolBioanalysisKit",
+ brew: "SW5E.ToolBrewerKit",
+ chef: "SW5E.ToolChefKit",
+ demo: "SW5E.ToolDemolitionKit",
+ disg: "SW5E.ToolDisguiseKit",
+ forg: "SW5E.ToolForgeryKit",
+ mech: "SW5E.ToolMechanicKit",
+ game: "SW5E.ToolGamingSet",
+ poi: "SW5E.ToolPoisonKit",
+ scav: "SW5E.ToolScavengingKit",
+ secur: "SW5E.ToolSecurityKit",
+ slic: "SW5E.ToolSlicerKit",
+ spice: "SW5E.ToolSpiceKit",
+ music: "SW5E.ToolMusicalInstrument",
+ vehicle: "SW5E.ToolVehicle"
};
// TODO: Same as weapon IDs
@@ -257,19 +256,18 @@ SW5E.toolIds = {
* @type {Object}
*/
SW5E.timePeriods = {
- "inst": "SW5E.TimeInst",
- "turn": "SW5E.TimeTurn",
- "round": "SW5E.TimeRound",
- "minute": "SW5E.TimeMinute",
- "hour": "SW5E.TimeHour",
- "day": "SW5E.TimeDay",
- "month": "SW5E.TimeMonth",
- "year": "SW5E.TimeYear",
- "perm": "SW5E.TimePerm",
- "spec": "SW5E.Special"
+ inst: "SW5E.TimeInst",
+ turn: "SW5E.TimeTurn",
+ round: "SW5E.TimeRound",
+ minute: "SW5E.TimeMinute",
+ hour: "SW5E.TimeHour",
+ day: "SW5E.TimeDay",
+ month: "SW5E.TimeMonth",
+ year: "SW5E.TimeYear",
+ perm: "SW5E.TimePerm",
+ spec: "SW5E.Special"
};
-
/* -------------------------------------------- */
/**
@@ -277,49 +275,47 @@ SW5E.timePeriods = {
* @type {Object}
*/
SW5E.abilityActivationTypes = {
- "none": "SW5E.None",
- "action": "SW5E.Action",
- "bonus": "SW5E.BonusAction",
- "reaction": "SW5E.Reaction",
- "minute": SW5E.timePeriods.minute,
- "hour": SW5E.timePeriods.hour,
- "day": SW5E.timePeriods.day,
- "special": SW5E.timePeriods.spec,
- "legendary": "SW5E.LegendaryActionLabel",
- "lair": "SW5E.LairActionLabel",
- "crew": "SW5E.VehicleCrewAction"
+ none: "SW5E.None",
+ action: "SW5E.Action",
+ bonus: "SW5E.BonusAction",
+ reaction: "SW5E.Reaction",
+ minute: SW5E.timePeriods.minute,
+ hour: SW5E.timePeriods.hour,
+ day: SW5E.timePeriods.day,
+ special: SW5E.timePeriods.spec,
+ legendary: "SW5E.LegendaryActionLabel",
+ lair: "SW5E.LairActionLabel",
+ crew: "SW5E.VehicleCrewAction"
};
/* -------------------------------------------- */
-
SW5E.abilityConsumptionTypes = {
- "ammo": "SW5E.ConsumeAmmunition",
- "attribute": "SW5E.ConsumeAttribute",
- "material": "SW5E.ConsumeMaterial",
- "charges": "SW5E.ConsumeCharges"
+ ammo: "SW5E.ConsumeAmmunition",
+ attribute: "SW5E.ConsumeAttribute",
+ material: "SW5E.ConsumeMaterial",
+ charges: "SW5E.ConsumeCharges"
};
-
/* -------------------------------------------- */
// Creature Sizes
SW5E.actorSizes = {
- "tiny": "SW5E.SizeTiny",
- "sm": "SW5E.SizeSmall",
- "med": "SW5E.SizeMedium",
- "lg": "SW5E.SizeLarge",
- "huge": "SW5E.SizeHuge",
- "grg": "SW5E.SizeGargantuan"
+ tiny: "SW5E.SizeTiny",
+ sm: "SW5E.SizeSmall",
+ med: "SW5E.SizeMedium",
+ lg: "SW5E.SizeLarge",
+ huge: "SW5E.SizeHuge",
+ grg: "SW5E.SizeGargantuan"
};
SW5E.tokenSizes = {
- "tiny": 1,
- "sm": 1,
- "med": 1,
- "lg": 2,
- "huge": 3,
- "grg": 4
+ tiny: 1,
+ sm: 1,
+ med: 1,
+ lg: 2,
+ huge: 3,
+ grg: 4
};
/**
@@ -327,10 +323,10 @@ SW5E.tokenSizes = {
* @enum {number}
*/
SW5E.tokenHPColors = {
- temp: 0x66CCFF,
- tempmax: 0x440066,
- negmax: 0x550000
-}
+ temp: 0x66ccff,
+ tempmax: 0x440066,
+ negmax: 0x550000
+};
/* -------------------------------------------- */
@@ -339,17 +335,16 @@ SW5E.tokenHPColors = {
* @type {Object}
*/
SW5E.creatureTypes = {
- "aberration": "SW5E.CreatureAberration",
- "beast": "SW5E.CreatureBeast",
- "construct": "SW5E.CreatureConstruct",
- "droid": "SW5E.CreatureDroid",
- "force": "SW5E.CreatureForceEntity",
- "humanoid": "SW5E.CreatureHumanoid",
- "plant": "SW5E.CreaturePlant",
- "undead": "SW5E.CreatureUndead"
+ aberration: "SW5E.CreatureAberration",
+ beast: "SW5E.CreatureBeast",
+ construct: "SW5E.CreatureConstruct",
+ droid: "SW5E.CreatureDroid",
+ force: "SW5E.CreatureForceEntity",
+ humanoid: "SW5E.CreatureHumanoid",
+ plant: "SW5E.CreaturePlant",
+ undead: "SW5E.CreatureUndead"
};
-
/* -------------------------------------------- */
/**
@@ -357,22 +352,22 @@ SW5E.creatureTypes = {
* @type {Object}
*/
SW5E.itemActionTypes = {
- "mwak": "SW5E.ActionMWAK",
- "rwak": "SW5E.ActionRWAK",
- "mpak": "SW5E.ActionMPAK",
- "rpak": "SW5E.ActionRPAK",
- "save": "SW5E.ActionSave",
- "heal": "SW5E.ActionHeal",
- "abil": "SW5E.ActionAbil",
- "util": "SW5E.ActionUtil",
- "other": "SW5E.ActionOther"
+ mwak: "SW5E.ActionMWAK",
+ rwak: "SW5E.ActionRWAK",
+ mpak: "SW5E.ActionMPAK",
+ rpak: "SW5E.ActionRPAK",
+ save: "SW5E.ActionSave",
+ heal: "SW5E.ActionHeal",
+ abil: "SW5E.ActionAbil",
+ util: "SW5E.ActionUtil",
+ other: "SW5E.ActionOther"
};
/* -------------------------------------------- */
SW5E.itemCapacityTypes = {
- "items": "SW5E.ItemContainerCapacityItems",
- "weight": "SW5E.ItemContainerCapacityWeight"
+ items: "SW5E.ItemContainerCapacityItems",
+ weight: "SW5E.ItemContainerCapacityWeight"
};
/* -------------------------------------------- */
@@ -382,15 +377,14 @@ SW5E.itemCapacityTypes = {
* @type {Object}
*/
SW5E.limitedUsePeriods = {
- "sr": "SW5E.ShortRest",
- "lr": "SW5E.LongRest",
- "day": "SW5E.Day",
- "charges": "SW5E.Charges",
- "recharge": "SW5E.Recharge",
- "refitting": "SW5E.Refitting"
+ sr: "SW5E.ShortRest",
+ lr: "SW5E.LongRest",
+ day: "SW5E.Day",
+ charges: "SW5E.Charges",
+ recharge: "SW5E.Recharge",
+ refitting: "SW5E.Refitting"
};
-
/* -------------------------------------------- */
/**
@@ -398,23 +392,22 @@ SW5E.limitedUsePeriods = {
* @type {Object}
*/
SW5E.equipmentTypes = {
- "light": "SW5E.EquipmentLight",
- "medium": "SW5E.EquipmentMedium",
- "heavy": "SW5E.EquipmentHeavy",
- "hyper": "SW5E.EquipmentHyperdrive",
- "bonus": "SW5E.EquipmentBonus",
- "natural": "SW5E.EquipmentNatural",
- "powerc": "SW5E.EquipmentPowerCoupling",
- "reactor": "SW5E.EquipmentReactor",
- "shield": "SW5E.EquipmentShield",
- "clothing": "SW5E.EquipmentClothing",
- "trinket": "SW5E.EquipmentTrinket",
- "ssarmor": "SW5E.EquipmentStarshipArmor",
- "ssshield": "SW5E.EquipmentStarshipShield",
- "vehicle": "SW5E.EquipmentVehicle"
+ light: "SW5E.EquipmentLight",
+ medium: "SW5E.EquipmentMedium",
+ heavy: "SW5E.EquipmentHeavy",
+ hyper: "SW5E.EquipmentHyperdrive",
+ bonus: "SW5E.EquipmentBonus",
+ natural: "SW5E.EquipmentNatural",
+ powerc: "SW5E.EquipmentPowerCoupling",
+ reactor: "SW5E.EquipmentReactor",
+ shield: "SW5E.EquipmentShield",
+ clothing: "SW5E.EquipmentClothing",
+ trinket: "SW5E.EquipmentTrinket",
+ ssarmor: "SW5E.EquipmentStarshipArmor",
+ ssshield: "SW5E.EquipmentStarshipShield",
+ vehicle: "SW5E.EquipmentVehicle"
};
-
/* -------------------------------------------- */
/**
@@ -422,10 +415,10 @@ SW5E.equipmentTypes = {
* @type {Object}
*/
SW5E.armorProficiencies = {
- "lgt": SW5E.equipmentTypes.light,
- "med": SW5E.equipmentTypes.medium,
- "hvy": SW5E.equipmentTypes.heavy,
- "shl": "SW5E.EquipmentShieldProficiency"
+ lgt: SW5E.equipmentTypes.light,
+ med: SW5E.equipmentTypes.medium,
+ hvy: SW5E.equipmentTypes.heavy,
+ shl: "SW5E.EquipmentShieldProficiency"
};
/**
@@ -433,14 +426,14 @@ SW5E.armorProficiencies = {
* Used when a new player owned item is created
* @type {Object}
*/
- SW5E.armorProficienciesMap = {
- "natural": true,
- "clothing": true,
- "light": "lgt",
- "medium": "med",
- "heavy": "hvy",
- "shield": "shl"
-}
+SW5E.armorProficienciesMap = {
+ natural: true,
+ clothing: true,
+ light: "lgt",
+ medium: "med",
+ heavy: "hvy",
+ shield: "shl"
+};
/* -------------------------------------------- */
@@ -449,16 +442,16 @@ SW5E.armorProficiencies = {
* @type {Object}
*/
SW5E.consumableTypes = {
- "adrenal": "SW5E.ConsumableAdrenal",
- "poison": "SW5E.ConsumablePoison",
- "explosive": "SW5E.ConsumableExplosive",
- "food": "SW5E.ConsumableFood",
- "medpac": "SW5E.ConsumableMedpac",
- "technology": "SW5E.ConsumableTechnology",
- "ammo": "SW5E.ConsumableAmmunition",
- "trinket": "SW5E.ConsumableTrinket",
- "force": "SW5E.ConsumableForce",
- "tech": "SW5E.ConsumableTech"
+ adrenal: "SW5E.ConsumableAdrenal",
+ poison: "SW5E.ConsumablePoison",
+ explosive: "SW5E.ConsumableExplosive",
+ food: "SW5E.ConsumableFood",
+ medpac: "SW5E.ConsumableMedpac",
+ technology: "SW5E.ConsumableTechnology",
+ ammo: "SW5E.ConsumableAmmunition",
+ trinket: "SW5E.ConsumableTrinket",
+ force: "SW5E.ConsumableForce",
+ tech: "SW5E.ConsumableTech"
};
/* -------------------------------------------- */
@@ -468,26 +461,25 @@ SW5E.consumableTypes = {
* @type {Object}
*/
SW5E.currencies = {
- "CR": "SW5E.CurrencyCR",
- };
+ CR: "SW5E.CurrencyCR"
+};
/* -------------------------------------------- */
-
// Damage Types
SW5E.damageTypes = {
- "acid": "SW5E.DamageAcid",
- "cold": "SW5E.DamageCold",
- "energy": "SW5E.DamageEnergy",
- "fire": "SW5E.DamageFire",
- "force": "SW5E.DamageForce",
- "ion": "SW5E.DamageIon",
- "kinetic": "SW5E.DamageKinetic",
- "lightning": "SW5E.DamageLightning",
- "necrotic": "SW5E.DamageNecrotic",
- "poison": "SW5E.DamagePoison",
- "psychic": "SW5E.DamagePsychic",
- "sonic": "SW5E.DamageSonic"
+ acid: "SW5E.DamageAcid",
+ cold: "SW5E.DamageCold",
+ energy: "SW5E.DamageEnergy",
+ fire: "SW5E.DamageFire",
+ force: "SW5E.DamageForce",
+ ion: "SW5E.DamageIon",
+ kinetic: "SW5E.DamageKinetic",
+ lightning: "SW5E.DamageLightning",
+ necrotic: "SW5E.DamageNecrotic",
+ poison: "SW5E.DamagePoison",
+ psychic: "SW5E.DamagePsychic",
+ sonic: "SW5E.DamageSonic"
};
// Damage Resistance Types
@@ -495,39 +487,38 @@ SW5E.damageResistanceTypes = foundry.utils.deepClone(SW5E.damageTypes);
/* -------------------------------------------- */
-
// armor Types
SW5E.armorPropertiesTypes = {
-"Absorptive": "SW5E.ArmorProperAbsorptive",
-"Agile": "SW5E.ArmorProperAgile",
-"Anchor": "SW5E.ArmorProperAnchor",
-"Avoidant": "SW5E.ArmorProperAvoidant",
-"Barbed": "SW5E.ArmorProperBarbed",
-"Bulky": "SW5E.ArmorProperBulky",
-"Charging": "SW5E.ArmorProperCharging",
-"Concealing": "SW5E.ArmorProperConcealing",
-"Cumbersome": "SW5E.ArmorProperCumbersome",
-"Gauntleted": "SW5E.ArmorProperGauntleted",
-"Imbalanced": "SW5E.ArmorProperImbalanced",
-"Impermeable": "SW5E.ArmorProperImpermeable",
-"Insulated": "SW5E.ArmorProperInsulated",
-"Interlocking": "SW5E.ArmorProperInterlocking",
-"Lambent": "SW5E.ArmorProperLambent",
-"Lightweight": "SW5E.ArmorProperLightweight",
-"Magnetic": "SW5E.ArmorProperMagnetic",
-"Obscured": "SW5E.ArmorProperObscured",
-"Obtrusive": "SW5E.ArmorProperObtrusive",
-"Powered": "SW5E.ArmorProperPowered",
-"Reactive": "SW5E.ArmorProperReactive",
-"Regulated": "SW5E.ArmorProperRegulated",
-"Reinforced": "SW5E.ArmorProperReinforced",
-"Responsive": "SW5E.ArmorProperResponsive",
-"Rigid": "SW5E.ArmorProperRigid",
-"Silent": "SW5E.ArmorProperSilent",
-"Spiked": "SW5E.ArmorProperSpiked",
-"Strength": "SW5E.ArmorProperStrength",
-"Steadfast": "SW5E.ArmorProperSteadfast",
-"Versatile": "SW5E.ArmorProperVersatile"
+ Absorptive: "SW5E.ArmorProperAbsorptive",
+ Agile: "SW5E.ArmorProperAgile",
+ Anchor: "SW5E.ArmorProperAnchor",
+ Avoidant: "SW5E.ArmorProperAvoidant",
+ Barbed: "SW5E.ArmorProperBarbed",
+ Bulky: "SW5E.ArmorProperBulky",
+ Charging: "SW5E.ArmorProperCharging",
+ Concealing: "SW5E.ArmorProperConcealing",
+ Cumbersome: "SW5E.ArmorProperCumbersome",
+ Gauntleted: "SW5E.ArmorProperGauntleted",
+ Imbalanced: "SW5E.ArmorProperImbalanced",
+ Impermeable: "SW5E.ArmorProperImpermeable",
+ Insulated: "SW5E.ArmorProperInsulated",
+ Interlocking: "SW5E.ArmorProperInterlocking",
+ Lambent: "SW5E.ArmorProperLambent",
+ Lightweight: "SW5E.ArmorProperLightweight",
+ Magnetic: "SW5E.ArmorProperMagnetic",
+ Obscured: "SW5E.ArmorProperObscured",
+ Obtrusive: "SW5E.ArmorProperObtrusive",
+ Powered: "SW5E.ArmorProperPowered",
+ Reactive: "SW5E.ArmorProperReactive",
+ Regulated: "SW5E.ArmorProperRegulated",
+ Reinforced: "SW5E.ArmorProperReinforced",
+ Responsive: "SW5E.ArmorProperResponsive",
+ Rigid: "SW5E.ArmorProperRigid",
+ Silent: "SW5E.ArmorProperSilent",
+ Spiked: "SW5E.ArmorProperSpiked",
+ Strength: "SW5E.ArmorProperStrength",
+ Steadfast: "SW5E.ArmorProperSteadfast",
+ Versatile: "SW5E.ArmorProperVersatile"
};
/**
@@ -536,15 +527,15 @@ SW5E.armorPropertiesTypes = {
* @type {Object}
*/
SW5E.movementTypes = {
- "burrow": "SW5E.MovementBurrow",
- "climb": "SW5E.MovementClimb",
- "crawl": "SW5E.MovementCrawl",
- "fly": "SW5E.MovementFly",
- "roll": "SW5E.MovementRoll",
- "space": "SW5E.MovementSpace",
- "swim": "SW5E.MovementSwim",
- "turn": "SW5E.MovementTurn",
- "walk": "SW5E.MovementWalk",
+ burrow: "SW5E.MovementBurrow",
+ climb: "SW5E.MovementClimb",
+ crawl: "SW5E.MovementCrawl",
+ fly: "SW5E.MovementFly",
+ roll: "SW5E.MovementRoll",
+ space: "SW5E.MovementSpace",
+ swim: "SW5E.MovementSwim",
+ turn: "SW5E.MovementTurn",
+ walk: "SW5E.MovementWalk"
};
/**
@@ -553,8 +544,8 @@ SW5E.movementTypes = {
* @type {Object}
*/
SW5E.movementUnits = {
- "ft": "SW5E.DistFt",
- "mi": "SW5E.DistMi"
+ ft: "SW5E.DistFt",
+ mi: "SW5E.DistMi"
};
/**
@@ -563,27 +554,26 @@ SW5E.movementUnits = {
* @type {Object}
*/
SW5E.distanceUnits = {
- "none": "SW5E.None",
- "self": "SW5E.DistSelf",
- "touch": "SW5E.DistTouch",
- "spec": "SW5E.Special",
- "any": "SW5E.DistAny"
+ none: "SW5E.None",
+ self: "SW5E.DistSelf",
+ touch: "SW5E.DistTouch",
+ spec: "SW5E.Special",
+ any: "SW5E.DistAny"
};
-for ( let [k, v] of Object.entries(SW5E.movementUnits) ) {
- SW5E.distanceUnits[k] = v;
+for (let [k, v] of Object.entries(SW5E.movementUnits)) {
+ SW5E.distanceUnits[k] = v;
}
/* -------------------------------------------- */
-
/**
* Configure aspects of encumbrance calculation so that it could be configured by modules
* @type {Object}
*/
SW5E.encumbrance = {
- currencyPerWeight: 50,
- strMultiplier: 15,
- vehicleWeightMultiplier: 2000 // 2000 lbs in a ton
+ currencyPerWeight: 50,
+ strMultiplier: 15,
+ vehicleWeightMultiplier: 2000 // 2000 lbs in a ton
};
/* -------------------------------------------- */
@@ -593,59 +583,54 @@ SW5E.encumbrance = {
* @type {Object}
*/
SW5E.targetTypes = {
- "none": "SW5E.None",
- "self": "SW5E.TargetSelf",
- "creature": "SW5E.TargetCreature",
- "droid": "SW5E.TargetDroid",
- "ally": "SW5E.TargetAlly",
- "enemy": "SW5E.TargetEnemy",
- "object": "SW5E.TargetObject",
- "space": "SW5E.TargetSpace",
- "radius": "SW5E.TargetRadius",
- "sphere": "SW5E.TargetSphere",
- "cylinder": "SW5E.TargetCylinder",
- "cone": "SW5E.TargetCone",
- "square": "SW5E.TargetSquare",
- "cube": "SW5E.TargetCube",
- "line": "SW5E.TargetLine",
- "starship": "SW5E.TargetStarship",
- "wall": "SW5E.TargetWall",
- "weapon": "SW5E.TargetWeapon"
+ none: "SW5E.None",
+ self: "SW5E.TargetSelf",
+ creature: "SW5E.TargetCreature",
+ droid: "SW5E.TargetDroid",
+ ally: "SW5E.TargetAlly",
+ enemy: "SW5E.TargetEnemy",
+ object: "SW5E.TargetObject",
+ space: "SW5E.TargetSpace",
+ radius: "SW5E.TargetRadius",
+ sphere: "SW5E.TargetSphere",
+ cylinder: "SW5E.TargetCylinder",
+ cone: "SW5E.TargetCone",
+ square: "SW5E.TargetSquare",
+ cube: "SW5E.TargetCube",
+ line: "SW5E.TargetLine",
+ starship: "SW5E.TargetStarship",
+ wall: "SW5E.TargetWall",
+ weapon: "SW5E.TargetWeapon"
};
-
/* -------------------------------------------- */
-
/**
* Map the subset of target types which produce a template area of effect
* The keys are SW5E target types and the values are MeasuredTemplate shape types
* @type {Object}
*/
SW5E.areaTargetTypes = {
- cone: "cone",
- cube: "rect",
- cylinder: "circle",
- line: "ray",
- radius: "circle",
- sphere: "circle",
- square: "rect",
- wall: "ray"
+ cone: "cone",
+ cube: "rect",
+ cylinder: "circle",
+ line: "ray",
+ radius: "circle",
+ sphere: "circle",
+ square: "rect",
+ wall: "ray"
};
-
/* -------------------------------------------- */
// Healing Types
SW5E.healingTypes = {
- "healing": "SW5E.Healing",
- "temphp": "SW5E.HealingTemp"
+ healing: "SW5E.Healing",
+ temphp: "SW5E.HealingTemp"
};
-
/* -------------------------------------------- */
-
/**
* Enumerate the denominations of hit dice which can apply to classes in the SW5E system
* @type {string[]}
@@ -654,29 +639,120 @@ SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"];
/* -------------------------------------------- */
-
/**
* Enumerate the denominations of power dice which can apply to starships in the SW5E system
* @enum {string}
*/
SW5E.powerDieTypes = [1, "d4", "d6", "d8", "d10", "d12"];
-
/* -------------------------------------------- */
/**
* Enumerate the base stat and feature settings for starships based on size.
- * @type {Array.}
+ * @type {Array.}
*/
SW5E.baseStarshipSettings = {
- "tiny": {"changes":[{"key":"data.abilities.dex.value","value":4,"mode":2,"priority":20},{"key":"data.abilities.dex.proficient","value":1,"mode":4,"priority":20}, {"key":"data.abilities.con.value","value":-4,"mode":2,"priority":20}, {"key":"data.abilities.int.proficient","value":1,"mode":4,"priority":20}], "attributes":{"crewcap":null, "hd":"1d4", "hp":{"value":4, "max":4, "temp":4, "tempmax":4}, "hsm":1, "sd":"1d4", "mods":{"open":10, "max":10}, "suites":{"open":0, "max":0}, "movement":{"fly":300, "turn":300}}},
- "sm": {"changes":[{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.dex.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":-2,"mode":2,"priority":20},{"key":"data.abilities.str.proficient","value":1,"mode":4,"priority":20}], "attributes":{"crewcap":1, "hd":"3d6", "hp":{"value":6, "max":6, "temp":6, "tempmax":6}, "hsm":2, "sd":"3d6", "mods":{"open":20, "max":20}, "suites":{"open":-1, "max":-1}, "movement":{"fly":300, "turn":250}}},
- "med": {"attributes":{"crewcap":1, "hd":"5d8", "hp":{"value":8, "max":8, "temp":8, "tempmax":8}, "hsm":3, "sd":"5d8", "mods":{"open":30, "max":30}, "suites":{"open":3, "max":3}, "movement":{"fly":300, "turn":200}}},
- "lg": {"changes":[{"key":"data.abilities.dex.value","value":-2,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":2,"mode":2,"priority":20}], "attributes":{"crewcap":200, "hd":"7d10", "hp":{"value":10, "max":10, "temp":10, "tempmax":10}, "hsm":4, "sd":"7d10", "mods":{"open":50, "max":50}, "suites":{"open":3, "max":3}, "movement":{"fly":300, "turn":150}}},
- "huge": {"changes":[{"key":"data.abilities.dex.value","value":-4,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":4,"mode":2,"priority":20}], "attributes":{"crewcap":4000, "hd":"9d12", "hp":{"value":12, "max":12, "temp":12, "tempmax":12}, "hsm":2, "sd":"9d12", "mods":{"open":60, "max":60}, "suites":{"open":6, "max":6}, "movement":{"fly":300, "turn":100}}},
- "grg": {"changes":[{"key":"data.abilities.dex.value","value":-6,"mode":2,"priority":20},{"key":"data.abilities.wis.proficient","value":1,"mode":4,"priority":20},{"key":"data.abilities.con.value","value":6,"mode":2,"priority":20}], "attributes":{"crewcap":80000, "hd":"11d20", "hp":{"value":20, "max":20, "temp":20, "tempmax":20}, "hsm":3, "sd":"11d20", "mods":{"open":70, "max":70}, "suites":{"open":10, "max":10}, "movement":{"fly":300, "turn":50}}}
-}
+ tiny: {
+ changes: [
+ {key: "data.abilities.dex.value", value: 4, mode: 2, priority: 20},
+ {key: "data.abilities.dex.proficient", value: 1, mode: 4, priority: 20},
+ {key: "data.abilities.con.value", value: -4, mode: 2, priority: 20},
+ {key: "data.abilities.int.proficient", value: 1, mode: 4, priority: 20}
+ ],
+ attributes: {
+ crewcap: null,
+ hd: "1d4",
+ hp: {value: 4, max: 4, temp: 4, tempmax: 4},
+ hsm: 1,
+ sd: "1d4",
+ mods: {open: 10, max: 10},
+ suites: {open: 0, max: 0},
+ movement: {fly: 300, turn: 300}
+ }
+ },
+ sm: {
+ changes: [
+ {key: "data.abilities.dex.value", value: 2, mode: 2, priority: 20},
+ {key: "data.abilities.dex.proficient", value: 1, mode: 4, priority: 20},
+ {key: "data.abilities.con.value", value: -2, mode: 2, priority: 20},
+ {key: "data.abilities.str.proficient", value: 1, mode: 4, priority: 20}
+ ],
+ attributes: {
+ crewcap: 1,
+ hd: "3d6",
+ hp: {value: 6, max: 6, temp: 6, tempmax: 6},
+ hsm: 2,
+ sd: "3d6",
+ mods: {open: 20, max: 20},
+ suites: {open: -1, max: -1},
+ movement: {fly: 300, turn: 250}
+ }
+ },
+ med: {
+ attributes: {
+ crewcap: 1,
+ hd: "5d8",
+ hp: {value: 8, max: 8, temp: 8, tempmax: 8},
+ hsm: 3,
+ sd: "5d8",
+ mods: {open: 30, max: 30},
+ suites: {open: 3, max: 3},
+ movement: {fly: 300, turn: 200}
+ }
+ },
+ lg: {
+ changes: [
+ {key: "data.abilities.dex.value", value: -2, mode: 2, priority: 20},
+ {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20},
+ {key: "data.abilities.con.value", value: 2, mode: 2, priority: 20}
+ ],
+ attributes: {
+ crewcap: 200,
+ hd: "7d10",
+ hp: {value: 10, max: 10, temp: 10, tempmax: 10},
+ hsm: 4,
+ sd: "7d10",
+ mods: {open: 50, max: 50},
+ suites: {open: 3, max: 3},
+ movement: {fly: 300, turn: 150}
+ }
+ },
+ huge: {
+ changes: [
+ {key: "data.abilities.dex.value", value: -4, mode: 2, priority: 20},
+ {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20},
+ {key: "data.abilities.con.value", value: 4, mode: 2, priority: 20}
+ ],
+ attributes: {
+ crewcap: 4000,
+ hd: "9d12",
+ hp: {value: 12, max: 12, temp: 12, tempmax: 12},
+ hsm: 2,
+ sd: "9d12",
+ mods: {open: 60, max: 60},
+ suites: {open: 6, max: 6},
+ movement: {fly: 300, turn: 100}
+ }
+ },
+ grg: {
+ changes: [
+ {key: "data.abilities.dex.value", value: -6, mode: 2, priority: 20},
+ {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20},
+ {key: "data.abilities.con.value", value: 6, mode: 2, priority: 20}
+ ],
+ attributes: {
+ crewcap: 80000,
+ hd: "11d20",
+ hp: {value: 20, max: 20, temp: 20, tempmax: 20},
+ hsm: 3,
+ sd: "11d20",
+ mods: {open: 70, max: 70},
+ suites: {open: 10, max: 10},
+ movement: {fly: 300, turn: 50}
+ }
+ }
+};
/* -------------------------------------------- */
@@ -685,47 +761,46 @@ SW5E.baseStarshipSettings = {
* @type {Object}
*/
- SW5E.starshipRolestiny = {
-};
+SW5E.starshipRolestiny = {};
SW5E.starshipRolessm = {
- "bmbr": "SW5E.StarshipBomber",
- "intc": "SW5E.StarshipInterceptor",
- "scout": "SW5E.StarshipScout",
- "scrm": "SW5E.StarshipScrambler",
- "shtl": "SW5E.StarshipShuttle",
- "strf": "SW5E.StarshipStrikeFighter"
+ bmbr: "SW5E.StarshipBomber",
+ intc: "SW5E.StarshipInterceptor",
+ scout: "SW5E.StarshipScout",
+ scrm: "SW5E.StarshipScrambler",
+ shtl: "SW5E.StarshipShuttle",
+ strf: "SW5E.StarshipStrikeFighter"
};
SW5E.starshipRolesmed = {
- "cour": "SW5E.StarshipCourier",
- "frtr": "SW5E.StarshipFreighter",
- "gnbt": "SW5E.StarshipGunboat",
- "msbt": "SW5E.StarshipMissileBoat",
- "nvgt": "SW5E.StarshipNavigator",
- "yacht": "SW5E.StarshipYacht"
+ cour: "SW5E.StarshipCourier",
+ frtr: "SW5E.StarshipFreighter",
+ gnbt: "SW5E.StarshipGunboat",
+ msbt: "SW5E.StarshipMissileBoat",
+ nvgt: "SW5E.StarshipNavigator",
+ yacht: "SW5E.StarshipYacht"
};
SW5E.starshipRoleslg = {
- "ambd": "SW5E.StarshipAmbassador",
- "corv": "SW5E.StarshipCorvette",
- "crui": "SW5E.StarshipCruiser",
- "expl": "SW5E.StarshipExplorer",
- "pics": "SW5E.StarshipPicketShip",
- "shtd": "SW5E.StarshipShipsTender"
+ ambd: "SW5E.StarshipAmbassador",
+ corv: "SW5E.StarshipCorvette",
+ crui: "SW5E.StarshipCruiser",
+ expl: "SW5E.StarshipExplorer",
+ pics: "SW5E.StarshipPicketShip",
+ shtd: "SW5E.StarshipShipsTender"
};
SW5E.starshipRoleshuge = {
- "btls": "SW5E.StarshipBattleship",
- "carr": "SW5E.StarshipCarrier",
- "colo": "SW5E.StarshipColonizer",
- "cmds": "SW5E.StarshipCommandShip",
- "intd": "SW5E.StarshipInterdictor",
- "jugg": "SW5E.StarshipJuggernaut"
+ btls: "SW5E.StarshipBattleship",
+ carr: "SW5E.StarshipCarrier",
+ colo: "SW5E.StarshipColonizer",
+ cmds: "SW5E.StarshipCommandShip",
+ intd: "SW5E.StarshipInterdictor",
+ jugg: "SW5E.StarshipJuggernaut"
};
SW5E.starshipRolesgrg = {
- "blks": "SW5E.StarshipBlockadeShip",
- "flgs": "SW5E.StarshipFlagship",
- "inct": "SW5E.StarshipIndustrialCenter",
- "mbmt": "SW5E.StarshipMobileMetropolis",
- "rsrc": "SW5E.StarshipResearcher",
- "wars": "SW5E.StarshipWarship"
+ blks: "SW5E.StarshipBlockadeShip",
+ flgs: "SW5E.StarshipFlagship",
+ inct: "SW5E.StarshipIndustrialCenter",
+ mbmt: "SW5E.StarshipMobileMetropolis",
+ rsrc: "SW5E.StarshipResearcher",
+ wars: "SW5E.StarshipWarship"
};
/* -------------------------------------------- */
@@ -735,52 +810,140 @@ SW5E.starshipRolesgrg = {
* @type {Object}
*/
- SW5E.starshipRoleBonuses = {
- "bmbr": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "intc": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]},
- "scout": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
- "scrm": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20}]},
- "shtl": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]},
- "strf": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "cour": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]},
- "frtr": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]},
- "gnbt": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "msbt": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "nvgt": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
- "yacht": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20}]},
- "ambd": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20}]},
- "corv": {"changes":[{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20}]},
- "crui": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "expl": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
- "pics": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "shtd": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "btls": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "carr": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
- "colo": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20}]},
- "cmds": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "intd": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "jugg": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "blks": {"changes":[{"key":"data.abilities.dex.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "flgs": {"changes":[{"key":"data.abilities.cha.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "inct": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]},
- "mbmt": {"changes":[{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "rsrc": {"changes":[{"key":"data.abilities.int.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20}]},
- "wars": {"changes":[{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20}]}
+SW5E.starshipRoleBonuses = {
+ bmbr: {changes: [{key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}]},
+ intc: {changes: [{key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}]},
+ scout: {changes: [{key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}]},
+ scrm: {changes: [{key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}]},
+ shtl: {changes: [{key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}]},
+ strf: {changes: [{key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}]},
+ cour: {changes: [{key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}]},
+ frtr: {changes: [{key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}]},
+ gnbt: {changes: [{key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}]},
+ msbt: {changes: [{key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}]},
+ nvgt: {changes: [{key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}]},
+ yacht: {changes: [{key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}]},
+ ambd: {
+ changes: [
+ {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ corv: {
+ changes: [
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ crui: {
+ changes: [
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ expl: {
+ changes: [
+ {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ pics: {
+ changes: [
+ {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ shtd: {
+ changes: [
+ {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ btls: {
+ changes: [
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ carr: {
+ changes: [
+ {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ colo: {
+ changes: [
+ {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ cmds: {
+ changes: [
+ {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ intd: {
+ changes: [
+ {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ jugg: {
+ changes: [
+ {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ blks: {
+ changes: [
+ {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ flgs: {
+ changes: [
+ {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ inct: {
+ changes: [
+ {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ mbmt: {
+ changes: [
+ {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ rsrc: {
+ changes: [
+ {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}
+ ]
+ },
+ wars: {
+ changes: [
+ {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20},
+ {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}
+ ]
+ }
};
/* -------------------------------------------- */
-
-
/**
* The set of possible sensory perception types which an Actor may have
* @enum {string}
*/
SW5E.senses = {
- "blindsight": "SW5E.SenseBlindsight",
- "darkvision": "SW5E.SenseDarkvision",
- "tremorsense": "SW5E.SenseTremorsense",
- "truesight": "SW5E.SenseTruesight"
+ blindsight: "SW5E.SenseBlindsight",
+ darkvision: "SW5E.SenseDarkvision",
+ tremorsense: "SW5E.SenseTremorsense",
+ truesight: "SW5E.SenseTruesight"
};
/* -------------------------------------------- */
@@ -790,24 +953,24 @@ SW5E.senses = {
* @type {Object}
*/
SW5E.skills = {
- "acr": "SW5E.SkillAcr",
- "ani": "SW5E.SkillAni",
- "ath": "SW5E.SkillAth",
- "dec": "SW5E.SkillDec",
- "ins": "SW5E.SkillIns",
- "itm": "SW5E.SkillItm",
- "inv": "SW5E.SkillInv",
- "lor": "SW5E.SkillLor",
- "med": "SW5E.SkillMed",
- "nat": "SW5E.SkillNat",
- "prc": "SW5E.SkillPrc",
- "prf": "SW5E.SkillPrf",
- "per": "SW5E.SkillPer",
- "pil": "SW5E.SkillPil",
- "slt": "SW5E.SkillSlt",
- "ste": "SW5E.SkillSte",
- "sur": "SW5E.SkillSur",
- "tec": "SW5E.SkillTec"
+ acr: "SW5E.SkillAcr",
+ ani: "SW5E.SkillAni",
+ ath: "SW5E.SkillAth",
+ dec: "SW5E.SkillDec",
+ ins: "SW5E.SkillIns",
+ itm: "SW5E.SkillItm",
+ inv: "SW5E.SkillInv",
+ lor: "SW5E.SkillLor",
+ med: "SW5E.SkillMed",
+ nat: "SW5E.SkillNat",
+ prc: "SW5E.SkillPrc",
+ prf: "SW5E.SkillPrf",
+ per: "SW5E.SkillPer",
+ pil: "SW5E.SkillPil",
+ slt: "SW5E.SkillSlt",
+ ste: "SW5E.SkillSte",
+ sur: "SW5E.SkillSur",
+ tec: "SW5E.SkillTec"
};
/* -------------------------------------------- */
@@ -817,29 +980,29 @@ SW5E.skills = {
* @type {Object}
*/
SW5E.starshipSkills = {
- "ast": "SW5E.StarshipSkillAst",
- "bst": "SW5E.StarshipSkillBst",
- "dat": "SW5E.StarshipSkillDat",
- "hid": "SW5E.StarshipSkillHid",
- "imp": "SW5E.StarshipSkillImp",
- "int": "SW5E.StarshipSkillInt",
- "man": "SW5E.StarshipSkillMan",
- "men": "SW5E.StarshipSkillMen",
- "pat": "SW5E.StarshipSkillPat",
- "prb": "SW5E.StarshipSkillPrb",
- "ram": "SW5E.StarshipSkillRam",
- "reg": "SW5E.StarshipSkillReg",
- "scn": "SW5E.StarshipSkillScn",
- "swn": "SW5E.StarshipSkillSwn"
+ ast: "SW5E.StarshipSkillAst",
+ bst: "SW5E.StarshipSkillBst",
+ dat: "SW5E.StarshipSkillDat",
+ hid: "SW5E.StarshipSkillHid",
+ imp: "SW5E.StarshipSkillImp",
+ int: "SW5E.StarshipSkillInt",
+ man: "SW5E.StarshipSkillMan",
+ men: "SW5E.StarshipSkillMen",
+ pat: "SW5E.StarshipSkillPat",
+ prb: "SW5E.StarshipSkillPrb",
+ ram: "SW5E.StarshipSkillRam",
+ reg: "SW5E.StarshipSkillReg",
+ scn: "SW5E.StarshipSkillScn",
+ swn: "SW5E.StarshipSkillSwn"
};
/* -------------------------------------------- */
SW5E.powerPreparationModes = {
- "prepared": "SW5E.PowerPrepPrepared",
- "always": "SW5E.PowerPrepAlways",
- "atwill": "SW5E.PowerPrepAtWill",
- "innate": "SW5E.PowerPrepInnate"
+ prepared: "SW5E.PowerPrepPrepared",
+ always: "SW5E.PowerPrepAlways",
+ atwill: "SW5E.PowerPrepAtWill",
+ innate: "SW5E.PowerPrepInnate"
};
SW5E.powerUpcastModes = ["always", "prepared"];
@@ -850,12 +1013,12 @@ SW5E.powerUpcastModes = ["always", "prepared"];
*/
SW5E.powerProgression = {
- "none": "SW5E.PowerNone",
- "consular": "SW5E.PowerProgCns",
- "engineer": "SW5E.PowerProgEng",
- "guardian": "SW5E.PowerProgGrd",
- "scout": "SW5E.PowerProgSct",
- "sentinel": "SW5E.PowerProgSnt"
+ none: "SW5E.PowerNone",
+ consular: "SW5E.PowerProgCns",
+ engineer: "SW5E.PowerProgEng",
+ guardian: "SW5E.PowerProgGrd",
+ scout: "SW5E.PowerProgSct",
+ sentinel: "SW5E.PowerProgSnt"
};
/**
@@ -863,12 +1026,12 @@ SW5E.powerProgression = {
*/
SW5E.powersKnown = {
- "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
- "consular": [9,11,13,15,17,19,21,23,25,26,28,29,31,32,34,35,37,38,39,40],
- "engineer": [6,7,9,10,12,13,15,16,18,19,21,22,23,24,25,26,27,28,29,30],
- "guardian": [5,7,9,10,12,13,14,15,17,18,19,20,22,23,24,25,27,28,29,30],
- "scout": [0,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23],
- "sentinel": [7,9,11,13,15,17,18,19,21,22,24,25,26,28,29,30,32,33,34,35]
+ none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ consular: [9, 11, 13, 15, 17, 19, 21, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 39, 40],
+ engineer: [6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
+ guardian: [5, 7, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25, 27, 28, 29, 30],
+ scout: [0, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
+ sentinel: [7, 9, 11, 13, 15, 17, 18, 19, 21, 22, 24, 25, 26, 28, 29, 30, 32, 33, 34, 35]
};
/**
@@ -876,14 +1039,14 @@ SW5E.powersKnown = {
*/
SW5E.powerLimit = {
- "none": [0,0,0,0,0,0,0,0,0],
- "consular": [1000,1000,1000,1000,1000,1,1,1,1],
- "engineer": [1000,1000,1000,1000,1000,1,1,1,1],
- "guardian": [1000,1000,1000,1000,1,0,0,0,0],
- "scout": [1000,1000,1000,1,1,0,0,0,0],
- "sentinel": [1000,1000,1000,1000,1,1,1,0,0],
- "innate": [1000,1000,1000,1000,1000,1000,1000,1000,1000],
- "dual": [1000,1000,1000,1000,1000,1,1,1,1]
+ none: [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ consular: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1],
+ engineer: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1],
+ guardian: [1000, 1000, 1000, 1000, 1, 0, 0, 0, 0],
+ scout: [1000, 1000, 1000, 1, 1, 0, 0, 0, 0],
+ sentinel: [1000, 1000, 1000, 1000, 1, 1, 1, 0, 0],
+ innate: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000],
+ dual: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1]
};
/**
@@ -891,15 +1054,15 @@ SW5E.powerLimit = {
*/
SW5E.powerMaxLevel = {
- "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
- "consular": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
- "engineer": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
- "guardian": [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5],
- "scout": [0,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5],
- "sentinel": [1,1,2,2,2,3,3,3,4,4,5,5,5,6,6,6,7,7,7,7],
- "multi": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
- "innate": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
- "dual": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9]
+ none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ consular: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9],
+ engineer: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9],
+ guardian: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5],
+ scout: [0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5],
+ sentinel: [1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7],
+ multi: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9],
+ innate: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9],
+ dual: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9]
};
/**
@@ -907,12 +1070,12 @@ SW5E.powerMaxLevel = {
*/
SW5E.powerPoints = {
- "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
- "consular": [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80],
- "engineer": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40],
- "guardian": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40],
- "scout": [0,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
- "sentinel": [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60]
+ none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ consular: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80],
+ engineer: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40],
+ guardian: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40],
+ scout: [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
+ sentinel: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60]
};
/* -------------------------------------------- */
@@ -922,37 +1085,34 @@ SW5E.powerPoints = {
* @type {Object}
*/
SW5E.powerScalingModes = {
- "none": "SW5E.PowerNone",
- "atwill": "SW5E.PowerAtWill",
- "level": "SW5E.PowerLevel"
+ none: "SW5E.PowerNone",
+ atwill: "SW5E.PowerAtWill",
+ level: "SW5E.PowerLevel"
};
/* -------------------------------------------- */
-
/**
* Define the set of types which a weapon item can take
* @type {Object}
*/
SW5E.weaponTypes = {
-
- "ammo": "SW5E.WeaponAmmo",
- "improv": "SW5E.WeaponImprov",
- "martialVW": "SW5E.WeaponMartialVW",
- "martialB": "SW5E.WeaponMartialB",
- "martialLW": "SW5E.WeaponMartialLW",
- "natural": "SW5E.WeaponNatural",
- "siege": "SW5E.WeaponSiege",
- "simpleVW": "SW5E.WeaponSimpleVW",
- "simpleB": "SW5E.WeaponSimpleB",
- "simpleLW": "SW5E.WeaponSimpleLW",
- "primary (starship)": "SW5E.WeaponPrimarySW",
- "secondary (starship)": "SW5E.WeaponSecondarySW",
- "tertiary (starship)": "SW5E.WeaponTertiarySW",
- "quaternary (starship)": "SW5E.WeaponQuaternarySW"
+ "ammo": "SW5E.WeaponAmmo",
+ "improv": "SW5E.WeaponImprov",
+ "martialVW": "SW5E.WeaponMartialVW",
+ "martialB": "SW5E.WeaponMartialB",
+ "martialLW": "SW5E.WeaponMartialLW",
+ "natural": "SW5E.WeaponNatural",
+ "siege": "SW5E.WeaponSiege",
+ "simpleVW": "SW5E.WeaponSimpleVW",
+ "simpleB": "SW5E.WeaponSimpleB",
+ "simpleLW": "SW5E.WeaponSimpleLW",
+ "primary (starship)": "SW5E.WeaponPrimarySW",
+ "secondary (starship)": "SW5E.WeaponSecondarySW",
+ "tertiary (starship)": "SW5E.WeaponTertiarySW",
+ "quaternary (starship)": "SW5E.WeaponQuaternarySW"
};
-
/* -------------------------------------------- */
/**
@@ -960,48 +1120,48 @@ SW5E.weaponTypes = {
* @type {Object}
*/
SW5E.weaponProperties = {
- "amm": "SW5E.WeaponPropertiesAmm",
- "aut": "SW5E.WeaponPropertiesAut",
- "bur": "SW5E.WeaponPropertiesBur",
- "con": "SW5E.WeaponPropertiesCon",
- "def": "SW5E.WeaponPropertiesDef",
- "dex": "SW5E.WeaponPropertiesDex",
- "dir": "SW5E.WeaponPropertiesDir",
- "drm": "SW5E.WeaponPropertiesDrm",
- "dgd": "SW5E.WeaponPropertiesDgd",
- "dis": "SW5E.WeaponPropertiesDis",
- "dpt": "SW5E.WeaponPropertiesDpt",
- "dou": "SW5E.WeaponPropertiesDou",
- "exp": "SW5E.WeaponPropertiesExp",
- "fin": "SW5E.WeaponPropertiesFin",
- "fix": "SW5E.WeaponPropertiesFix",
- "foc": "SW5E.WeaponPropertiesFoc",
- "hvy": "SW5E.WeaponPropertiesHvy",
- "hid": "SW5E.WeaponPropertiesHid",
- "hom": "SW5E.WeaponPropertiesHom",
- "ion": "SW5E.WeaponPropertiesIon",
- "ken": "SW5E.WeaponPropertiesKen",
- "lgt": "SW5E.WeaponPropertiesLgt",
- "lum": "SW5E.WeaponPropertiesLum",
- "mlt": "SW5E.WeaponPropertiesMlt",
- "mig": "SW5E.WeaponPropertiesMig",
- "ovr": "SW5E.WeaponPropertiesOvr",
- "pic": "SW5E.WeaponPropertiesPic",
- "pow": "SW5E.WeaponPropertiesPow",
- "rap": "SW5E.WeaponPropertiesRap",
- "rch": "SW5E.WeaponPropertiesRch",
- "rel": "SW5E.WeaponPropertiesRel",
- "ret": "SW5E.WeaponPropertiesRet",
- "sat": "SW5E.WeaponPropertiesSat",
- "shk": "SW5E.WeaponPropertiesShk",
- "sil": "SW5E.WeaponPropertiesSil",
- "spc": "SW5E.WeaponPropertiesSpc",
- "str": "SW5E.WeaponPropertiesStr",
- "thr": "SW5E.WeaponPropertiesThr",
- "two": "SW5E.WeaponPropertiesTwo",
- "ver": "SW5E.WeaponPropertiesVer",
- "vic": "SW5E.WeaponPropertiesVic",
- "zon": "SW5E.WeaponPropertiesZon"
+ amm: "SW5E.WeaponPropertiesAmm",
+ aut: "SW5E.WeaponPropertiesAut",
+ bur: "SW5E.WeaponPropertiesBur",
+ con: "SW5E.WeaponPropertiesCon",
+ def: "SW5E.WeaponPropertiesDef",
+ dex: "SW5E.WeaponPropertiesDex",
+ dir: "SW5E.WeaponPropertiesDir",
+ drm: "SW5E.WeaponPropertiesDrm",
+ dgd: "SW5E.WeaponPropertiesDgd",
+ dis: "SW5E.WeaponPropertiesDis",
+ dpt: "SW5E.WeaponPropertiesDpt",
+ dou: "SW5E.WeaponPropertiesDou",
+ exp: "SW5E.WeaponPropertiesExp",
+ fin: "SW5E.WeaponPropertiesFin",
+ fix: "SW5E.WeaponPropertiesFix",
+ foc: "SW5E.WeaponPropertiesFoc",
+ hvy: "SW5E.WeaponPropertiesHvy",
+ hid: "SW5E.WeaponPropertiesHid",
+ hom: "SW5E.WeaponPropertiesHom",
+ ion: "SW5E.WeaponPropertiesIon",
+ ken: "SW5E.WeaponPropertiesKen",
+ lgt: "SW5E.WeaponPropertiesLgt",
+ lum: "SW5E.WeaponPropertiesLum",
+ mlt: "SW5E.WeaponPropertiesMlt",
+ mig: "SW5E.WeaponPropertiesMig",
+ ovr: "SW5E.WeaponPropertiesOvr",
+ pic: "SW5E.WeaponPropertiesPic",
+ pow: "SW5E.WeaponPropertiesPow",
+ rap: "SW5E.WeaponPropertiesRap",
+ rch: "SW5E.WeaponPropertiesRch",
+ rel: "SW5E.WeaponPropertiesRel",
+ ret: "SW5E.WeaponPropertiesRet",
+ sat: "SW5E.WeaponPropertiesSat",
+ shk: "SW5E.WeaponPropertiesShk",
+ sil: "SW5E.WeaponPropertiesSil",
+ spc: "SW5E.WeaponPropertiesSpc",
+ str: "SW5E.WeaponPropertiesStr",
+ thr: "SW5E.WeaponPropertiesThr",
+ two: "SW5E.WeaponPropertiesTwo",
+ ver: "SW5E.WeaponPropertiesVer",
+ vic: "SW5E.WeaponPropertiesVic",
+ zon: "SW5E.WeaponPropertiesZon"
};
/* -------------------------------------------- */
@@ -1011,42 +1171,42 @@ SW5E.weaponProperties = {
* @type {Object}
*/
SW5E.weaponSizes = {
- "tiny": "SW5E.SizeTiny",
- "sm": "SW5E.SizeSmall",
- "med": "SW5E.SizeMedium",
- "lg": "SW5E.SizeLarge",
- "huge": "SW5E.SizeHuge",
- "grg": "SW5E.SizeGargantuan"
-};
+ tiny: "SW5E.SizeTiny",
+ sm: "SW5E.SizeSmall",
+ med: "SW5E.SizeMedium",
+ lg: "SW5E.SizeLarge",
+ huge: "SW5E.SizeHuge",
+ grg: "SW5E.SizeGargantuan"
+};
// Power Components
SW5E.powerComponents = {
- "V": "SW5E.ComponentVerbal",
- "S": "SW5E.ComponentSomatic",
- "M": "SW5E.ComponentMaterial"
+ V: "SW5E.ComponentVerbal",
+ S: "SW5E.ComponentSomatic",
+ M: "SW5E.ComponentMaterial"
};
// Power Schools
SW5E.powerSchools = {
- "lgt": "SW5E.SchoolLgt",
- "uni": "SW5E.SchoolUni",
- "drk": "SW5E.SchoolDrk",
- "tec": "SW5E.SchoolTec",
- "enh": "SW5E.SchoolEnh"
+ lgt: "SW5E.SchoolLgt",
+ uni: "SW5E.SchoolUni",
+ drk: "SW5E.SchoolDrk",
+ tec: "SW5E.SchoolTec",
+ enh: "SW5E.SchoolEnh"
};
// Power Levels
SW5E.powerLevels = {
- 0: "SW5E.PowerLevel0",
- 1: "SW5E.PowerLevel1",
- 2: "SW5E.PowerLevel2",
- 3: "SW5E.PowerLevel3",
- 4: "SW5E.PowerLevel4",
- 5: "SW5E.PowerLevel5",
- 6: "SW5E.PowerLevel6",
- 7: "SW5E.PowerLevel7",
- 8: "SW5E.PowerLevel8",
- 9: "SW5E.PowerLevel9"
+ 0: "SW5E.PowerLevel0",
+ 1: "SW5E.PowerLevel1",
+ 2: "SW5E.PowerLevel2",
+ 3: "SW5E.PowerLevel3",
+ 4: "SW5E.PowerLevel4",
+ 5: "SW5E.PowerLevel5",
+ 6: "SW5E.PowerLevel6",
+ 7: "SW5E.PowerLevel7",
+ 8: "SW5E.PowerLevel8",
+ 9: "SW5E.PowerLevel9"
};
// TODO: This is used for spell scrolls, it maps the level to the compendium ID of the item the spell would be bound to
@@ -1072,23 +1232,23 @@ SW5E.powerScrollIds = {
* @enum {string}
*/
SW5E.sourcePacks = {
- ITEMS: "sw5e.items"
-}
+ ITEMS: "sw5e.items"
+};
// Polymorph options.
SW5E.polymorphSettings = {
- keepPhysical: 'SW5E.PolymorphKeepPhysical',
- keepMental: 'SW5E.PolymorphKeepMental',
- keepSaves: 'SW5E.PolymorphKeepSaves',
- keepSkills: 'SW5E.PolymorphKeepSkills',
- mergeSaves: 'SW5E.PolymorphMergeSaves',
- mergeSkills: 'SW5E.PolymorphMergeSkills',
- keepClass: 'SW5E.PolymorphKeepClass',
- keepFeats: 'SW5E.PolymorphKeepFeats',
- keepPowers: 'SW5E.PolymorphKeepPowers',
- keepItems: 'SW5E.PolymorphKeepItems',
- keepBio: 'SW5E.PolymorphKeepBio',
- keepVision: 'SW5E.PolymorphKeepVision'
+ keepPhysical: "SW5E.PolymorphKeepPhysical",
+ keepMental: "SW5E.PolymorphKeepMental",
+ keepSaves: "SW5E.PolymorphKeepSaves",
+ keepSkills: "SW5E.PolymorphKeepSkills",
+ mergeSaves: "SW5E.PolymorphMergeSaves",
+ mergeSkills: "SW5E.PolymorphMergeSkills",
+ keepClass: "SW5E.PolymorphKeepClass",
+ keepFeats: "SW5E.PolymorphKeepFeats",
+ keepPowers: "SW5E.PolymorphKeepPowers",
+ keepItems: "SW5E.PolymorphKeepItems",
+ keepBio: "SW5E.PolymorphKeepBio",
+ keepVision: "SW5E.PolymorphKeepVision"
};
/* -------------------------------------------- */
@@ -1099,10 +1259,10 @@ SW5E.polymorphSettings = {
* @type {Object}
*/
SW5E.proficiencyLevels = {
- 0: "SW5E.NotProficient",
- 1: "SW5E.Proficient",
- 0.5: "SW5E.HalfProficient",
- 2: "SW5E.Expertise"
+ 0: "SW5E.NotProficient",
+ 1: "SW5E.Proficient",
+ 0.5: "SW5E.HalfProficient",
+ 2: "SW5E.Expertise"
};
/* -------------------------------------------- */
@@ -1113,157 +1273,156 @@ SW5E.proficiencyLevels = {
* in play, we take the highest value.
*/
SW5E.cover = {
- 0: 'SW5E.None',
- .5: 'SW5E.CoverHalf',
- .75: 'SW5E.CoverThreeQuarters',
- 1: 'SW5E.CoverTotal'
+ 0: "SW5E.None",
+ 0.5: "SW5E.CoverHalf",
+ 0.75: "SW5E.CoverThreeQuarters",
+ 1: "SW5E.CoverTotal"
};
/* -------------------------------------------- */
-
// Condition Types
SW5E.conditionTypes = {
- "blinded": "SW5E.ConBlinded",
- "charmed": "SW5E.ConCharmed",
- "deafened": "SW5E.ConDeafened",
- "diseased": "SW5E.ConDiseased",
- "exhaustion": "SW5E.ConExhaustion",
- "frightened": "SW5E.ConFrightened",
- "grappled": "SW5E.ConGrappled",
- "incapacitated": "SW5E.ConIncapacitated",
- "invisible": "SW5E.ConInvisible",
- "paralyzed": "SW5E.ConParalyzed",
- "petrified": "SW5E.ConPetrified",
- "poisoned": "SW5E.ConPoisoned",
- "prone": "SW5E.ConProne",
- "restrained": "SW5E.ConRestrained",
- "shocked": "SW5E.ConShocked",
- "slowed": "SW5E.ConSlowed",
- "stunned": "SW5E.ConStunned",
- "unconscious": "SW5E.ConUnconscious"
+ blinded: "SW5E.ConBlinded",
+ charmed: "SW5E.ConCharmed",
+ deafened: "SW5E.ConDeafened",
+ diseased: "SW5E.ConDiseased",
+ exhaustion: "SW5E.ConExhaustion",
+ frightened: "SW5E.ConFrightened",
+ grappled: "SW5E.ConGrappled",
+ incapacitated: "SW5E.ConIncapacitated",
+ invisible: "SW5E.ConInvisible",
+ paralyzed: "SW5E.ConParalyzed",
+ petrified: "SW5E.ConPetrified",
+ poisoned: "SW5E.ConPoisoned",
+ prone: "SW5E.ConProne",
+ restrained: "SW5E.ConRestrained",
+ shocked: "SW5E.ConShocked",
+ slowed: "SW5E.ConSlowed",
+ stunned: "SW5E.ConStunned",
+ unconscious: "SW5E.ConUnconscious"
};
// Languages
SW5E.languages = {
- "abyssin": "SW5E.LanguagesAbyssin",
- "aleena": "SW5E.LanguagesAleena",
- "antarian": "SW5E.LanguagesAntarian",
- "anzellan": "SW5E.LanguagesAnzellan",
- "aqualish": "SW5E.LanguagesAqualish",
- "arconese": "SW5E.LanguagesArconese",
- "ardennian": "SW5E.LanguagesArdennian",
- "arkanian": "SW5E.LanguagesArkanian",
- "balosur": "SW5E.LanguagesBalosur",
- "barabel": "SW5E.LanguagesBarabel",
- "basic": "SW5E.LanguagesBasic",
- "besalisk": "SW5E.LanguagesBesalisk",
- "binary": "SW5E.LanguagesBinary",
- "bith": "SW5E.LanguagesBith",
- "bocce": "SW5E.LanguagesBocce",
- "bothese": "SW5E.LanguagesBothese",
- "catharese": "SW5E.LanguagesCatharese",
- "cerean": "SW5E.LanguagesCerean",
- "chadra-fan": "SW5E.LanguagesChadra-Fan",
- "chagri": "SW5E.LanguagesChagri",
- "cheunh": "SW5E.LanguagesCheunh",
- "chevin": "SW5E.LanguagesChevin",
- "chironan": "SW5E.LanguagesChironan",
- "clawdite": "SW5E.LanguagesClawdite",
- "codruese": "SW5E.LanguagesCodruese",
- "colicoid": "SW5E.LanguagesColicoid",
- "dashadi": "SW5E.LanguagesDashadi",
- "defel": "SW5E.LanguagesDefel",
- "devaronese": "SW5E.LanguagesDevaronese",
- "dosh": "SW5E.LanguagesDosh",
- "draethos": "SW5E.LanguagesDraethos",
- "durese": "SW5E.LanguagesDurese",
- "dug": "SW5E.LanguagesDug",
- "ewokese": "SW5E.LanguagesEwokese",
- "falleen": "SW5E.LanguagesFalleen",
- "felucianese": "SW5E.LanguagesFelucianese",
- "gamorrese": "SW5E.LanguagesGamorrese",
- "gand": "SW5E.LanguagesGand",
- "geonosian": "SW5E.LanguagesGeonosian",
- "givin": "SW5E.LanguagesGivin",
- "gran": "SW5E.LanguagesGran",
- "gungan": "SW5E.LanguagesGungan",
- "hapan": "SW5E.LanguagesHapan",
- "harchese": "SW5E.LanguagesHarchese",
- "herglese": "SW5E.LanguagesHerglese",
- "honoghran": "SW5E.LanguagesHonoghran",
- "huttese": "SW5E.LanguagesHuttese",
- "iktotchese": "SW5E.LanguagesIktotchese",
- "ithorese": "SW5E.LanguagesIthorese",
- "jawaese": "SW5E.LanguagesJawaese",
- "kaleesh": "SW5E.LanguagesKaleesh",
- "kaminoan": "SW5E.LanguagesKaminoan",
- "karkaran": "SW5E.LanguagesKarkaran",
- "keldor": "SW5E.LanguagesKelDor",
- "kharan": "SW5E.LanguagesKharan",
- "killik": "SW5E.LanguagesKillik",
- "klatooinian": "SW5E.LanguagesKlatooinian",
- "kubazian": "SW5E.LanguagesKubazian",
- "kushiban": "SW5E.LanguagesKushiban",
- "kyuzo": "SW5E.LanguagesKyuzo",
- "lannik": "SW5E.LanguagesLannik",
- "lasat": "SW5E.LanguagesLasat",
- "lowickese": "SW5E.LanguagesLowickese",
- "lurmese": "SW5E.LanguagesLurmese",
- "mandoa": "SW5E.LanguagesMandoa",
- "miralukese": "SW5E.LanguagesMiralukese",
- "mirialan": "SW5E.LanguagesMirialan",
- "moncal": "SW5E.LanguagesMonCal",
- "mustafarian": "SW5E.LanguagesMustafarian",
- "muun": "SW5E.LanguagesMuun",
- "nautila": "SW5E.LanguagesNautila",
- "ortolan": "SW5E.LanguagesOrtolan",
- "pakpak": "SW5E.LanguagesPakPak",
- "pyke": "SW5E.LanguagesPyke",
- "quarrenese": "SW5E.LanguagesQuarrenese",
- "rakata": "SW5E.LanguagesRakata",
- "rattataki": "SW5E.LanguagesRattataki",
- "rishii": "SW5E.LanguagesRishii",
- "rodese": "SW5E.LanguagesRodese",
- "ryn": "SW5E.LanguagesRyn",
- "selkatha": "SW5E.LanguagesSelkatha",
- "semblan": "SW5E.LanguagesSemblan",
- "shistavanen": "SW5E.LanguagesShistavanen",
- "shyriiwook": "SW5E.LanguagesShyriiwook",
- "sith": "SW5E.LanguagesSith",
- "squibbian": "SW5E.LanguagesSquibbian",
- "sriluurian": "SW5E.LanguagesSriluurian",
- "ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi",
- "sullustese": "SW5E.LanguagesSullustese",
- "talzzi": "SW5E.LanguagesTalzzi",
- "tarasinese": "SW5E.LanguagesTarasinese",
- "thisspiasian": "SW5E.LanguagesThisspiasian",
- "togorese": "SW5E.LanguagesTogorese",
- "togruti": "SW5E.LanguagesTogruti",
- "toydarian": "SW5E.LanguagesToydarian",
- "tusken": "SW5E.LanguagesTusken",
- "twi'leki": "SW5E.LanguagesTwileki",
- "ugnaught": "SW5E.LanguagesUgnaught",
- "umbaran": "SW5E.LanguagesUmbaran",
- "utapese": "SW5E.LanguagesUtapese",
- "verpine": "SW5E.LanguagesVerpine",
- "vong": "SW5E.LanguagesVong",
- "voss": "SW5E.LanguagesVoss",
- "yevethan": "SW5E.LanguagesYevethan",
- "zabraki": "SW5E.LanguagesZabraki",
- "zygerrian": "SW5E.LanguagesZygerrian"
+ "abyssin": "SW5E.LanguagesAbyssin",
+ "aleena": "SW5E.LanguagesAleena",
+ "antarian": "SW5E.LanguagesAntarian",
+ "anzellan": "SW5E.LanguagesAnzellan",
+ "aqualish": "SW5E.LanguagesAqualish",
+ "arconese": "SW5E.LanguagesArconese",
+ "ardennian": "SW5E.LanguagesArdennian",
+ "arkanian": "SW5E.LanguagesArkanian",
+ "balosur": "SW5E.LanguagesBalosur",
+ "barabel": "SW5E.LanguagesBarabel",
+ "basic": "SW5E.LanguagesBasic",
+ "besalisk": "SW5E.LanguagesBesalisk",
+ "binary": "SW5E.LanguagesBinary",
+ "bith": "SW5E.LanguagesBith",
+ "bocce": "SW5E.LanguagesBocce",
+ "bothese": "SW5E.LanguagesBothese",
+ "catharese": "SW5E.LanguagesCatharese",
+ "cerean": "SW5E.LanguagesCerean",
+ "chadra-fan": "SW5E.LanguagesChadra-Fan",
+ "chagri": "SW5E.LanguagesChagri",
+ "cheunh": "SW5E.LanguagesCheunh",
+ "chevin": "SW5E.LanguagesChevin",
+ "chironan": "SW5E.LanguagesChironan",
+ "clawdite": "SW5E.LanguagesClawdite",
+ "codruese": "SW5E.LanguagesCodruese",
+ "colicoid": "SW5E.LanguagesColicoid",
+ "dashadi": "SW5E.LanguagesDashadi",
+ "defel": "SW5E.LanguagesDefel",
+ "devaronese": "SW5E.LanguagesDevaronese",
+ "dosh": "SW5E.LanguagesDosh",
+ "draethos": "SW5E.LanguagesDraethos",
+ "durese": "SW5E.LanguagesDurese",
+ "dug": "SW5E.LanguagesDug",
+ "ewokese": "SW5E.LanguagesEwokese",
+ "falleen": "SW5E.LanguagesFalleen",
+ "felucianese": "SW5E.LanguagesFelucianese",
+ "gamorrese": "SW5E.LanguagesGamorrese",
+ "gand": "SW5E.LanguagesGand",
+ "geonosian": "SW5E.LanguagesGeonosian",
+ "givin": "SW5E.LanguagesGivin",
+ "gran": "SW5E.LanguagesGran",
+ "gungan": "SW5E.LanguagesGungan",
+ "hapan": "SW5E.LanguagesHapan",
+ "harchese": "SW5E.LanguagesHarchese",
+ "herglese": "SW5E.LanguagesHerglese",
+ "honoghran": "SW5E.LanguagesHonoghran",
+ "huttese": "SW5E.LanguagesHuttese",
+ "iktotchese": "SW5E.LanguagesIktotchese",
+ "ithorese": "SW5E.LanguagesIthorese",
+ "jawaese": "SW5E.LanguagesJawaese",
+ "kaleesh": "SW5E.LanguagesKaleesh",
+ "kaminoan": "SW5E.LanguagesKaminoan",
+ "karkaran": "SW5E.LanguagesKarkaran",
+ "keldor": "SW5E.LanguagesKelDor",
+ "kharan": "SW5E.LanguagesKharan",
+ "killik": "SW5E.LanguagesKillik",
+ "klatooinian": "SW5E.LanguagesKlatooinian",
+ "kubazian": "SW5E.LanguagesKubazian",
+ "kushiban": "SW5E.LanguagesKushiban",
+ "kyuzo": "SW5E.LanguagesKyuzo",
+ "lannik": "SW5E.LanguagesLannik",
+ "lasat": "SW5E.LanguagesLasat",
+ "lowickese": "SW5E.LanguagesLowickese",
+ "lurmese": "SW5E.LanguagesLurmese",
+ "mandoa": "SW5E.LanguagesMandoa",
+ "miralukese": "SW5E.LanguagesMiralukese",
+ "mirialan": "SW5E.LanguagesMirialan",
+ "moncal": "SW5E.LanguagesMonCal",
+ "mustafarian": "SW5E.LanguagesMustafarian",
+ "muun": "SW5E.LanguagesMuun",
+ "nautila": "SW5E.LanguagesNautila",
+ "ortolan": "SW5E.LanguagesOrtolan",
+ "pakpak": "SW5E.LanguagesPakPak",
+ "pyke": "SW5E.LanguagesPyke",
+ "quarrenese": "SW5E.LanguagesQuarrenese",
+ "rakata": "SW5E.LanguagesRakata",
+ "rattataki": "SW5E.LanguagesRattataki",
+ "rishii": "SW5E.LanguagesRishii",
+ "rodese": "SW5E.LanguagesRodese",
+ "ryn": "SW5E.LanguagesRyn",
+ "selkatha": "SW5E.LanguagesSelkatha",
+ "semblan": "SW5E.LanguagesSemblan",
+ "shistavanen": "SW5E.LanguagesShistavanen",
+ "shyriiwook": "SW5E.LanguagesShyriiwook",
+ "sith": "SW5E.LanguagesSith",
+ "squibbian": "SW5E.LanguagesSquibbian",
+ "sriluurian": "SW5E.LanguagesSriluurian",
+ "ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi",
+ "sullustese": "SW5E.LanguagesSullustese",
+ "talzzi": "SW5E.LanguagesTalzzi",
+ "tarasinese": "SW5E.LanguagesTarasinese",
+ "thisspiasian": "SW5E.LanguagesThisspiasian",
+ "togorese": "SW5E.LanguagesTogorese",
+ "togruti": "SW5E.LanguagesTogruti",
+ "toydarian": "SW5E.LanguagesToydarian",
+ "tusken": "SW5E.LanguagesTusken",
+ "twi'leki": "SW5E.LanguagesTwileki",
+ "ugnaught": "SW5E.LanguagesUgnaught",
+ "umbaran": "SW5E.LanguagesUmbaran",
+ "utapese": "SW5E.LanguagesUtapese",
+ "verpine": "SW5E.LanguagesVerpine",
+ "vong": "SW5E.LanguagesVong",
+ "voss": "SW5E.LanguagesVoss",
+ "yevethan": "SW5E.LanguagesYevethan",
+ "zabraki": "SW5E.LanguagesZabraki",
+ "zygerrian": "SW5E.LanguagesZygerrian"
};
// Character Level XP Requirements
-SW5E.CHARACTER_EXP_LEVELS = [
- 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000,
- 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]
-;
+SW5E.CHARACTER_EXP_LEVELS = [
+ 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000,
+ 265000, 305000, 355000
+];
// Challenge Rating XP Levels
SW5E.CR_EXP_LEVELS = [
- 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000,
- 20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
+ 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000, 20000,
+ 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
];
// Character Features Per Class And Level
@@ -1271,354 +1430,354 @@ SW5E.classFeatures = ClassFeatures;
// Configure Optional Character Flags
SW5E.characterFlags = {
- "adaptiveResilience": {
- name: "SW5E.FlagsAdaptiveResilience",
- hint: "SW5E.FlagsAdaptiveResilienceHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "aggressive": {
- name: "SW5E.FlagsAggressive",
- hint: "SW5E.FlagsAggressiveHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "amphibious": {
- name: "SW5E.FlagsAmphibious",
- hint: "SW5E.FlagsAmphibiousHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "armorIntegration": {
- name: "SW5E.FlagsArmorIntegration",
- hint: "SW5E.FlagsArmorIntegrationHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "businessSavvy": {
- name: "SW5E.FlagsBusinessSavvy",
- hint: "SW5E.FlagsBusinessSavvyHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "cannibalize": {
- name: "SW5E.FlagsCannibalize",
- hint: "SW5E.FlagsCannibalizeHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "closedMind": {
- name: "SW5E.FlagsClosedMind",
- hint: "SW5E.FlagsClosedMindHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "crudeWeaponSpecialists": {
- name: "SW5E.FlagsCrudeWeaponSpecialists",
- hint: "SW5E.FlagsCrudeWeaponSpecialistsHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "defiant": {
- name: "SW5E.FlagsDefiant",
- hint: "SW5E.FlagsDefiantHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "detailOriented": {
- name: "SW5E.FlagsDetailOriented",
- hint: "SW5E.FlagsDetailOrientedHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "enthrallingPheromones": {
- name: "SW5E.FlagsEnthrallingPheromones",
- hint: "SW5E.FlagsEnthrallingPheromonesHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "extraArms": {
- name: "SW5E.FlagsExtraArms",
- hint: "SW5E.FlagsExtraArmsHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "forceContention": {
- name: "SW5E.FlagsForceContention",
- hint: "SW5E.FlagsForceContentionHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "forceInsensitive": {
- name: "SW5E.FlagsForceInsensitive",
- hint: "SW5E.FlagsForceInsensitiveHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "foreignBiology": {
- name: "SW5E.FlagsForeignBiology",
- hint: "SW5E.FlagsForeignBiologyHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "furyOfTheSmall": {
- name: "SW5E.FlagsFuryOfTheSmall",
- hint: "SW5E.FlagsFuryOfTheSmallHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "grovelCowerAndBeg": {
- name: "SW5E.FlagsGrovelCowerAndBeg",
- hint: "SW5E.FlagsGrovelCowerAndBegHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "inscrutable": {
- name: "SW5E.FlagsInscrutable",
- hint: "SW5E.FlagsInscrutableHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "keenSenses": {
- name: "SW5E.FlagsKeenSenses",
- hint: "SW5E.FlagsKeenSensesHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "longlimbed": {
- name: "SW5E.FlagsLongLimbed",
- hint: "SW5E.FlagsLongLimbedHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "maintenanceMode": {
- name: "SW5E.FlagsMaintenanceMode",
- hint: "SW5E.FlagsMaintenanceModeHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "maskOfTheWild": {
- name: "SW5E.FlagsMaskOfTheWild",
- hint: "SW5E.FlagsMaskOfTheWildHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "multipleHearts": {
- name: "SW5E.FlagsMultipleHearts",
- hint: "SW5E.FlagsMultipleHeartsHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "naturallyStealthy": {
- name: "SW5E.FlagsNaturallyStealthy",
- hint: "SW5E.FlagsNaturallyStealthyHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "nimbleAgility": {
- name: "SW5E.FlagsNimbleAgility",
- hint: "SW5E.FlagsNimbleAgilityHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "nimbleEscape": {
- name: "SW5E.FlagsNimbleEscape",
- hint: "SW5E.FlagsNimbleEscapeHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "nimbleness": {
- name: "SW5E.FlagsNimbleness",
- hint: "SW5E.FlagsNimblenessHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "pintsized": {
- name: "SW5E.FlagsPintsized",
- hint: "SW5E.FlagsPintsizedHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "powerfulBuild": {
- name: "SW5E.FlagsPowerfulBuild",
- hint: "SW5E.FlagsPowerfulBuildHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "precognition": {
- name: "SW5E.FlagsPrecognition",
- hint: "SW5E.FlagsPrecognitionHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "programmer": {
- name: "SW5E.FlagsProgrammer",
- hint: "SW5E.FlagsProgrammerHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "puny": {
- name: "SW5E.FlagsPuny",
- hint: "SW5E.FlagsPunyHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "rapidReconstruction": {
- name: "SW5E.FlagsRapidReconstruction",
- hint: "SW5E.FlagsRapidReconstructionHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "rapidlyRegenerative": {
- name: "SW5E.FlagsRapidlyRegenerative",
- hint: "SW5E.FlagsRapidlyRegenerativeHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "regenerative": {
- name: "SW5E.FlagsRegenerative",
- hint: "SW5E.FlagsRegenerativeHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "savageAttacks": {
- name: "SW5E.FlagsSavageAttacks",
- hint: "SW5E.FlagsSavageAttacksHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "shapechanger": {
- name: "SW5E.FlagsShapechanger",
- hint: "SW5E.FlagsShapechangerHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "strongLegged": {
- name: "SW5E.FlagsStrongLegged",
- hint: "SW5E.FlagsStrongLeggedHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "sunlightSensitivity": {
- name: "SW5E.FlagsSunlightSensitivity",
- hint: "SW5E.FlagsSunlightSensitivityHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "surpriseAttack": {
- name: "SW5E.FlagsSurpriseAttack",
- hint: "SW5E.FlagsSurpriseAttackHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "techImpaired": {
- name: "SW5E.FlagsTechImpaired",
- hint: "SW5E.FlagsTechImpairedHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "techResistance": {
- name: "SW5E.FlagsTechResistance",
- hint: "SW5E.FlagsTechResistanceHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "tinker": {
- name: "SW5E.FlagsTinker",
- hint: "SW5E.FlagsTinkerHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "toughness": {
- name: "SW5E.FlagsToughness",
- hint: "SW5E.FlagsToughnessHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "trance": {
- name: "SW5E.FlagsTrance",
- hint: "SW5E.FlagsTranceHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "unarmedCombatant": {
- name: "SW5E.FlagsUnarmedCombatant",
- hint: "SW5E.FlagsUnarmedCombatantHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "undersized": {
- name: "SW5E.FlagsUndersized",
- hint: "SW5E.FlagsUndersizedHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "unsettlingVisage": {
- name: "SW5E.FlagsUnsettlingVisage",
- hint: "SW5E.FlagsUnsettlingVisageHint",
- section: "SW5E.SpeciesTraits",
- type: Boolean
- },
- "initiativeAdv": {
- name: "SW5E.FlagsInitiativeAdv",
- hint: "SW5E.FlagsInitiativeAdvHint",
- section: "SW5E.Features",
- type: Boolean
- },
- "initiativeAlert": {
- name: "SW5E.FlagsAlert",
- hint: "SW5E.FlagsAlertHint",
- section: "SW5E.Features",
- type: Boolean
- },
- "jackOfAllTrades": {
- name: "SW5E.FlagsJOAT",
- hint: "SW5E.FlagsJOATHint",
- section: "SW5E.Features",
- type: Boolean
- },
- "observantFeat": {
- name: "SW5E.FlagsObservant",
- hint: "SW5E.FlagsObservantHint",
- skills: ['prc','inv'],
- section: "SW5E.Features",
- type: Boolean
- },
- "reliableTalent": {
- name: "SW5E.FlagsReliableTalent",
- hint: "SW5E.FlagsReliableTalentHint",
- section: "SW5E.Features",
- type: Boolean
- },
- "remarkableAthlete": {
- name: "SW5E.FlagsRemarkableAthlete",
- hint: "SW5E.FlagsRemarkableAthleteHint",
- abilities: ['str','dex','con'],
- section: "SW5E.Features",
- type: Boolean
- },
- "weaponCriticalThreshold": {
- name: "SW5E.FlagsWeaponCritThreshold",
- hint: "SW5E.FlagsWeaponCritThresholdHint",
- section: "SW5E.Features",
- type: Number,
- placeholder: 20
- },
- "powerCriticalThreshold": {
- name: "SW5E.FlagsPowerCritThreshold",
- hint: "SW5E.FlagsPowerCritThresholdHint",
- section: "SW5E.Features",
- type: Number,
- placeholder: 20
- },
- "meleeCriticalDamageDice": {
- name: "SW5E.FlagsMeleeCriticalDice",
- hint: "SW5E.FlagsMeleeCriticalDiceHint",
- section: "SW5E.Features",
- type: Number,
- placeholder: 0
- }
+ adaptiveResilience: {
+ name: "SW5E.FlagsAdaptiveResilience",
+ hint: "SW5E.FlagsAdaptiveResilienceHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ aggressive: {
+ name: "SW5E.FlagsAggressive",
+ hint: "SW5E.FlagsAggressiveHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ amphibious: {
+ name: "SW5E.FlagsAmphibious",
+ hint: "SW5E.FlagsAmphibiousHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ armorIntegration: {
+ name: "SW5E.FlagsArmorIntegration",
+ hint: "SW5E.FlagsArmorIntegrationHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ businessSavvy: {
+ name: "SW5E.FlagsBusinessSavvy",
+ hint: "SW5E.FlagsBusinessSavvyHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ cannibalize: {
+ name: "SW5E.FlagsCannibalize",
+ hint: "SW5E.FlagsCannibalizeHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ closedMind: {
+ name: "SW5E.FlagsClosedMind",
+ hint: "SW5E.FlagsClosedMindHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ crudeWeaponSpecialists: {
+ name: "SW5E.FlagsCrudeWeaponSpecialists",
+ hint: "SW5E.FlagsCrudeWeaponSpecialistsHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ defiant: {
+ name: "SW5E.FlagsDefiant",
+ hint: "SW5E.FlagsDefiantHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ detailOriented: {
+ name: "SW5E.FlagsDetailOriented",
+ hint: "SW5E.FlagsDetailOrientedHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ enthrallingPheromones: {
+ name: "SW5E.FlagsEnthrallingPheromones",
+ hint: "SW5E.FlagsEnthrallingPheromonesHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ extraArms: {
+ name: "SW5E.FlagsExtraArms",
+ hint: "SW5E.FlagsExtraArmsHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ forceContention: {
+ name: "SW5E.FlagsForceContention",
+ hint: "SW5E.FlagsForceContentionHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ forceInsensitive: {
+ name: "SW5E.FlagsForceInsensitive",
+ hint: "SW5E.FlagsForceInsensitiveHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ foreignBiology: {
+ name: "SW5E.FlagsForeignBiology",
+ hint: "SW5E.FlagsForeignBiologyHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ furyOfTheSmall: {
+ name: "SW5E.FlagsFuryOfTheSmall",
+ hint: "SW5E.FlagsFuryOfTheSmallHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ grovelCowerAndBeg: {
+ name: "SW5E.FlagsGrovelCowerAndBeg",
+ hint: "SW5E.FlagsGrovelCowerAndBegHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ inscrutable: {
+ name: "SW5E.FlagsInscrutable",
+ hint: "SW5E.FlagsInscrutableHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ keenSenses: {
+ name: "SW5E.FlagsKeenSenses",
+ hint: "SW5E.FlagsKeenSensesHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ longlimbed: {
+ name: "SW5E.FlagsLongLimbed",
+ hint: "SW5E.FlagsLongLimbedHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ maintenanceMode: {
+ name: "SW5E.FlagsMaintenanceMode",
+ hint: "SW5E.FlagsMaintenanceModeHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ maskOfTheWild: {
+ name: "SW5E.FlagsMaskOfTheWild",
+ hint: "SW5E.FlagsMaskOfTheWildHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ multipleHearts: {
+ name: "SW5E.FlagsMultipleHearts",
+ hint: "SW5E.FlagsMultipleHeartsHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ naturallyStealthy: {
+ name: "SW5E.FlagsNaturallyStealthy",
+ hint: "SW5E.FlagsNaturallyStealthyHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ nimbleAgility: {
+ name: "SW5E.FlagsNimbleAgility",
+ hint: "SW5E.FlagsNimbleAgilityHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ nimbleEscape: {
+ name: "SW5E.FlagsNimbleEscape",
+ hint: "SW5E.FlagsNimbleEscapeHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ nimbleness: {
+ name: "SW5E.FlagsNimbleness",
+ hint: "SW5E.FlagsNimblenessHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ pintsized: {
+ name: "SW5E.FlagsPintsized",
+ hint: "SW5E.FlagsPintsizedHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ powerfulBuild: {
+ name: "SW5E.FlagsPowerfulBuild",
+ hint: "SW5E.FlagsPowerfulBuildHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ precognition: {
+ name: "SW5E.FlagsPrecognition",
+ hint: "SW5E.FlagsPrecognitionHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ programmer: {
+ name: "SW5E.FlagsProgrammer",
+ hint: "SW5E.FlagsProgrammerHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ puny: {
+ name: "SW5E.FlagsPuny",
+ hint: "SW5E.FlagsPunyHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ rapidReconstruction: {
+ name: "SW5E.FlagsRapidReconstruction",
+ hint: "SW5E.FlagsRapidReconstructionHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ rapidlyRegenerative: {
+ name: "SW5E.FlagsRapidlyRegenerative",
+ hint: "SW5E.FlagsRapidlyRegenerativeHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ regenerative: {
+ name: "SW5E.FlagsRegenerative",
+ hint: "SW5E.FlagsRegenerativeHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ savageAttacks: {
+ name: "SW5E.FlagsSavageAttacks",
+ hint: "SW5E.FlagsSavageAttacksHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ shapechanger: {
+ name: "SW5E.FlagsShapechanger",
+ hint: "SW5E.FlagsShapechangerHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ strongLegged: {
+ name: "SW5E.FlagsStrongLegged",
+ hint: "SW5E.FlagsStrongLeggedHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ sunlightSensitivity: {
+ name: "SW5E.FlagsSunlightSensitivity",
+ hint: "SW5E.FlagsSunlightSensitivityHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ surpriseAttack: {
+ name: "SW5E.FlagsSurpriseAttack",
+ hint: "SW5E.FlagsSurpriseAttackHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ techImpaired: {
+ name: "SW5E.FlagsTechImpaired",
+ hint: "SW5E.FlagsTechImpairedHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ techResistance: {
+ name: "SW5E.FlagsTechResistance",
+ hint: "SW5E.FlagsTechResistanceHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ tinker: {
+ name: "SW5E.FlagsTinker",
+ hint: "SW5E.FlagsTinkerHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ toughness: {
+ name: "SW5E.FlagsToughness",
+ hint: "SW5E.FlagsToughnessHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ trance: {
+ name: "SW5E.FlagsTrance",
+ hint: "SW5E.FlagsTranceHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ unarmedCombatant: {
+ name: "SW5E.FlagsUnarmedCombatant",
+ hint: "SW5E.FlagsUnarmedCombatantHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ undersized: {
+ name: "SW5E.FlagsUndersized",
+ hint: "SW5E.FlagsUndersizedHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ unsettlingVisage: {
+ name: "SW5E.FlagsUnsettlingVisage",
+ hint: "SW5E.FlagsUnsettlingVisageHint",
+ section: "SW5E.SpeciesTraits",
+ type: Boolean
+ },
+ initiativeAdv: {
+ name: "SW5E.FlagsInitiativeAdv",
+ hint: "SW5E.FlagsInitiativeAdvHint",
+ section: "SW5E.Features",
+ type: Boolean
+ },
+ initiativeAlert: {
+ name: "SW5E.FlagsAlert",
+ hint: "SW5E.FlagsAlertHint",
+ section: "SW5E.Features",
+ type: Boolean
+ },
+ jackOfAllTrades: {
+ name: "SW5E.FlagsJOAT",
+ hint: "SW5E.FlagsJOATHint",
+ section: "SW5E.Features",
+ type: Boolean
+ },
+ observantFeat: {
+ name: "SW5E.FlagsObservant",
+ hint: "SW5E.FlagsObservantHint",
+ skills: ["prc", "inv"],
+ section: "SW5E.Features",
+ type: Boolean
+ },
+ reliableTalent: {
+ name: "SW5E.FlagsReliableTalent",
+ hint: "SW5E.FlagsReliableTalentHint",
+ section: "SW5E.Features",
+ type: Boolean
+ },
+ remarkableAthlete: {
+ name: "SW5E.FlagsRemarkableAthlete",
+ hint: "SW5E.FlagsRemarkableAthleteHint",
+ abilities: ["str", "dex", "con"],
+ section: "SW5E.Features",
+ type: Boolean
+ },
+ weaponCriticalThreshold: {
+ name: "SW5E.FlagsWeaponCritThreshold",
+ hint: "SW5E.FlagsWeaponCritThresholdHint",
+ section: "SW5E.Features",
+ type: Number,
+ placeholder: 20
+ },
+ powerCriticalThreshold: {
+ name: "SW5E.FlagsPowerCritThreshold",
+ hint: "SW5E.FlagsPowerCritThresholdHint",
+ section: "SW5E.Features",
+ type: Number,
+ placeholder: 20
+ },
+ meleeCriticalDamageDice: {
+ name: "SW5E.FlagsMeleeCriticalDice",
+ hint: "SW5E.FlagsMeleeCriticalDiceHint",
+ section: "SW5E.Features",
+ type: Number,
+ placeholder: 0
+ }
};
// Configure allowed status flags
-SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags));
\ No newline at end of file
+SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags));
diff --git a/module/dice.js b/module/dice.js
index fc70522b..8abc0291 100644
--- a/module/dice.js
+++ b/module/dice.js
@@ -12,50 +12,55 @@ export {default as DamageRoll} from "./dice/damage-roll.js";
* @return {string} The resulting simplified formula
*/
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
- const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
- const terms = roll.terms;
+ const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
+ const terms = roll.terms;
- // Some terms are "too complicated" for this algorithm to simplify
- // In this case, the original formula is returned.
- if (terms.some(_isUnsupportedTerm)) return roll.formula;
+ // Some terms are "too complicated" for this algorithm to simplify
+ // In this case, the original formula is returned.
+ if (terms.some(_isUnsupportedTerm)) return roll.formula;
- const rollableTerms = []; // Terms that are non-constant, and their associated operators
- const constantTerms = []; // Terms that are constant, and their associated operators
- let operators = []; // Temporary storage for operators before they are moved to one of the above
+ const rollableTerms = []; // Terms that are non-constant, and their associated operators
+ const constantTerms = []; // Terms that are constant, and their associated operators
+ let operators = []; // Temporary storage for operators before they are moved to one of the above
- for (let term of terms) { // For each term
- if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
- else { // Otherwise the term is not an operator
- if (term instanceof DiceTerm) { // If the term is something rollable
- rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
- rollableTerms.push(term); // Then place this rollable term into it as well
- } //
- else { // Otherwise, this must be a constant
- constantTerms.push(...operators); // Place the operators into the constantTerms array
- constantTerms.push(term); // Then also add this constant term to that array.
- } //
- operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
+ for (let term of terms) {
+ // For each term
+ if (term instanceof OperatorTerm) operators.push(term);
+ // If the term is an addition/subtraction operator, push the term into the operators array
+ else {
+ // Otherwise the term is not an operator
+ if (term instanceof DiceTerm) {
+ // If the term is something rollable
+ rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
+ rollableTerms.push(term); // Then place this rollable term into it as well
+ } //
+ else {
+ // Otherwise, this must be a constant
+ constantTerms.push(...operators); // Place the operators into the constantTerms array
+ constantTerms.push(term); // Then also add this constant term to that array.
+ } //
+ operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
+ }
}
- }
- const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
- const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
+ const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
+ const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
- // Mathematically evaluate the constant formula to produce a single constant term
- let constantPart = undefined;
- if ( constantFormula ) {
- try {
- constantPart = Roll.safeEval(constantFormula)
- } catch (err) {
- console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
+ // Mathematically evaluate the constant formula to produce a single constant term
+ let constantPart = undefined;
+ if (constantFormula) {
+ try {
+ constantPart = Roll.safeEval(constantFormula);
+ } catch (err) {
+ console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
+ }
}
- }
- // Order the rollable and constant terms, either constant first or second depending on the optional argument
- const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
+ // Order the rollable and constant terms, either constant first or second depending on the optional argument
+ const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
- // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
- return new Roll(parts.filterJoin(" + ")).formula;
+ // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
+ return new Roll(parts.filterJoin(" + ")).formula;
}
/* -------------------------------------------- */
@@ -66,11 +71,11 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
* @return {Boolean} True when unsupported, false if supported
*/
function _isUnsupportedTerm(term) {
- const diceTerm = term instanceof DiceTerm;
- const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
- const number = term instanceof NumericTerm;
+ const diceTerm = term instanceof DiceTerm;
+ const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
+ const number = term instanceof NumericTerm;
- return !(diceTerm || operator || number);
+ return !(diceTerm || operator || number);
}
/* -------------------------------------------- */
@@ -111,54 +116,75 @@ function _isUnsupportedTerm(term) {
* @return {Promise} The evaluated D20Roll, or null if the workflow was cancelled
*/
export async function d20Roll({
- parts=[], data={}, // Roll creation
- advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization
- chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration
- chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization
- }={}) {
-
- // Handle input arguments
- const formula = ["1d20"].concat(parts).join(" + ");
- const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
- const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
- if ( chooseModifier && !isFF ) data["mod"] = "@mod";
-
- // Construct the D20Roll instance
- const roll = new CONFIG.Dice.D20Roll(formula, data, {
- flavor: flavor || title,
- advantageMode,
- defaultRollMode,
- critical,
- fumble,
+ parts = [],
+ data = {}, // Roll creation
+ advantage,
+ disadvantage,
+ fumble = 1,
+ critical = 20,
targetValue,
elvenAccuracy,
halflingLucky,
- reliableTalent
- });
+ reliableTalent, // Roll customization
+ chooseModifier = false,
+ fastForward = false,
+ event,
+ template,
+ title,
+ dialogOptions, // Dialog configuration
+ chatMessage = true,
+ messageData = {},
+ rollMode,
+ speaker,
+ flavor // Chat Message customization
+} = {}) {
+ // Handle input arguments
+ const formula = ["1d20"].concat(parts).join(" + ");
+ const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
+ const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
+ if (chooseModifier && !isFF) data["mod"] = "@mod";
- // Prompt a Dialog to further configure the D20Roll
- if ( !isFF ) {
- const configured = await roll.configureDialog({
- title,
- chooseModifier,
- defaultRollMode: defaultRollMode,
- defaultAction: advantageMode,
- defaultAbility: data?.item?.ability,
- template
- }, dialogOptions);
- if ( configured === null ) return null;
- }
+ // Construct the D20Roll instance
+ const roll = new CONFIG.Dice.D20Roll(formula, data, {
+ flavor: flavor || title,
+ advantageMode,
+ defaultRollMode,
+ critical,
+ fumble,
+ targetValue,
+ elvenAccuracy,
+ halflingLucky,
+ reliableTalent
+ });
- // Evaluate the configured roll
- await roll.evaluate({async: true});
+ // Prompt a Dialog to further configure the D20Roll
+ if (!isFF) {
+ const configured = await roll.configureDialog(
+ {
+ title,
+ chooseModifier,
+ defaultRollMode: defaultRollMode,
+ defaultAction: advantageMode,
+ defaultAbility: data?.item?.ability,
+ template
+ },
+ dialogOptions
+ );
+ if (configured === null) return null;
+ }
- // Create a Chat Message
- if ( speaker ) {
- console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`);
- messageData.speaker = speaker;
- }
- if ( roll && chatMessage ) await roll.toMessage(messageData);
- return roll;
+ // Evaluate the configured roll
+ await roll.evaluate({async: true});
+
+ // Create a Chat Message
+ if (speaker) {
+ console.warn(
+ `You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
+ );
+ messageData.speaker = speaker;
+ }
+ if (roll && chatMessage) await roll.toMessage(messageData);
+ return roll;
}
/* -------------------------------------------- */
@@ -167,12 +193,13 @@ export async function d20Roll({
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
*/
-function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) {
- const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
- let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
- if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
- else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
- return {isFF, advantageMode};
+function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
+ const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
+ let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
+ if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
+ else if (disadvantage || event?.ctrlKey || event?.metaKey)
+ advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
+ return {isFF, advantageMode};
}
/* -------------------------------------------- */
@@ -210,49 +237,67 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa
* @return {Promise} The evaluated DamageRoll, or null if the workflow was canceled
*/
export async function damageRoll({
- parts=[], data, // Roll creation
- critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization
- fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration
- chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization
- }={}) {
-
- // Handle input arguments
- const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
-
- // Construct the DamageRoll instance
- const formula = parts.join(" + ");
- const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
- const roll = new CONFIG.Dice.DamageRoll(formula, data, {
- flavor: flavor || title,
- critical: isCritical,
+ parts = [],
+ data, // Roll creation
+ critical = false,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
- powerfulCritical
- });
+ powerfulCritical, // Damage customization
+ fastForward = false,
+ event,
+ allowCritical = true,
+ template,
+ title,
+ dialogOptions, // Dialog configuration
+ chatMessage = true,
+ messageData = {},
+ rollMode,
+ speaker,
+ flavor // Chat Message customization
+} = {}) {
+ // Handle input arguments
+ const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
- // Prompt a Dialog to further configure the DamageRoll
- if ( !isFF ) {
- const configured = await roll.configureDialog({
- title,
- defaultRollMode: defaultRollMode,
- defaultCritical: isCritical,
- template,
- allowCritical
- }, dialogOptions);
- if ( configured === null ) return null;
- }
+ // Construct the DamageRoll instance
+ const formula = parts.join(" + ");
+ const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
+ const roll = new CONFIG.Dice.DamageRoll(formula, data, {
+ flavor: flavor || title,
+ critical: isCritical,
+ criticalBonusDice,
+ criticalMultiplier,
+ multiplyNumeric,
+ powerfulCritical
+ });
- // Evaluate the configured roll
- await roll.evaluate({async: true});
+ // Prompt a Dialog to further configure the DamageRoll
+ if (!isFF) {
+ const configured = await roll.configureDialog(
+ {
+ title,
+ defaultRollMode: defaultRollMode,
+ defaultCritical: isCritical,
+ template,
+ allowCritical
+ },
+ dialogOptions
+ );
+ if (configured === null) return null;
+ }
- // Create a Chat Message
- if ( speaker ) {
- console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`);
- messageData.speaker = speaker;
- }
- if ( roll && chatMessage ) await roll.toMessage(messageData);
- return roll;
+ // Evaluate the configured roll
+ await roll.evaluate({async: true});
+
+ // Create a Chat Message
+ if (speaker) {
+ console.warn(
+ `You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
+ );
+ messageData.speaker = speaker;
+ }
+ if (roll && chatMessage) await roll.toMessage(messageData);
+ return roll;
}
/* -------------------------------------------- */
@@ -261,8 +306,8 @@ export async function damageRoll({
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
*/
-function _determineCriticalMode({event, critical=false, fastForward=false}={}) {
- const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
- if ( event?.altKey ) critical = true;
- return {isFF, isCritical: critical};
+function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
+ const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
+ if (event?.altKey) critical = true;
+ return {isFF, isCritical: critical};
}
diff --git a/module/dice/d20-roll.js b/module/dice/d20-roll.js
index c4b40824..fb8a18b0 100644
--- a/module/dice/d20-roll.js
+++ b/module/dice/d20-roll.js
@@ -16,7 +16,7 @@
export default class D20Roll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
- if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) {
+ if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
}
this.configureModifiers();
@@ -31,8 +31,8 @@ export default class D20Roll extends Roll {
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
- DISADVANTAGE: -1,
- }
+ DISADVANTAGE: -1
+ };
/**
* The HTML template path used to configure evaluation of this Roll
@@ -71,28 +71,26 @@ export default class D20Roll extends Roll {
d20.modifiers = [];
// Halfling Lucky
- if ( this.options.halflingLucky ) d20.modifiers.push("r1=1");
+ if (this.options.halflingLucky) d20.modifiers.push("r1=1");
// Reliable Talent
- if ( this.options.reliableTalent ) d20.modifiers.push("min10");
+ if (this.options.reliableTalent) d20.modifiers.push("min10");
// Handle Advantage or Disadvantage
- if ( this.hasAdvantage ) {
+ if (this.hasAdvantage) {
d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh");
d20.options.advantage = true;
- }
- else if ( this.hasDisadvantage ) {
+ } else if (this.hasDisadvantage) {
d20.number = 2;
d20.modifiers.push("kl");
d20.options.disadvantage = true;
- }
- else d20.number = 1;
+ } else d20.number = 1;
// Assign critical and fumble thresholds
- if ( this.options.critical ) d20.options.critical = this.options.critical;
- if ( this.options.fumble ) d20.options.fumble = this.options.fumble;
- if ( this.options.targetValue ) d20.options.target = this.options.targetValue;
+ if (this.options.critical) d20.options.critical = this.options.critical;
+ if (this.options.fumble) d20.options.fumble = this.options.fumble;
+ if (this.options.targetValue) d20.options.target = this.options.targetValue;
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
@@ -101,22 +99,21 @@ export default class D20Roll extends Roll {
/* -------------------------------------------- */
/** @inheritdoc */
- async toMessage(messageData={}, options={}) {
-
+ async toMessage(messageData = {}, options = {}) {
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
- if ( !this._evaluated ) await this.evaluate({async: true});
+ if (!this._evaluated) await this.evaluate({async: true});
// Add appropriate advantage mode message flavor and sw5e roll flags
messageData.flavor = messageData.flavor || this.options.flavor;
- if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
- else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
+ if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
+ else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
// Add reliable talent to the d20-term flavor text if it applied
- if ( this.options.reliableTalent ) {
+ if (this.options.reliableTalent) {
const d20 = this.dice[0];
- const isRT = d20.results.every(r => !r.active || (r.result < 10));
+ const isRT = d20.results.every((r) => !r.active || r.result < 10);
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
- if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
+ if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
}
// Record the preferred rollMode
@@ -140,8 +137,17 @@ export default class D20Roll extends Roll {
* @param {object} options Additional Dialog customization options
* @returns {Promise} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
- async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) {
-
+ async configureDialog(
+ {
+ title,
+ defaultRollMode,
+ defaultAction = D20Roll.ADV_MODE.NORMAL,
+ chooseModifier = false,
+ defaultAbility,
+ template
+ } = {},
+ options = {}
+ ) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
@@ -154,32 +160,39 @@ export default class D20Roll extends Roll {
let defaultButton = "normal";
switch (defaultAction) {
- case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break;
- case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break;
+ case D20Roll.ADV_MODE.ADVANTAGE:
+ defaultButton = "advantage";
+ break;
+ case D20Roll.ADV_MODE.DISADVANTAGE:
+ defaultButton = "disadvantage";
+ break;
}
// Create the Dialog window and await submission of the form
- return new Promise(resolve => {
- new Dialog({
- title,
- content,
- buttons: {
- advantage: {
- label: game.i18n.localize("SW5E.Advantage"),
- callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
+ return new Promise((resolve) => {
+ new Dialog(
+ {
+ title,
+ content,
+ buttons: {
+ advantage: {
+ label: game.i18n.localize("SW5E.Advantage"),
+ callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
+ },
+ normal: {
+ label: game.i18n.localize("SW5E.Normal"),
+ callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
+ },
+ disadvantage: {
+ label: game.i18n.localize("SW5E.Disadvantage"),
+ callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
+ }
},
- normal: {
- label: game.i18n.localize("SW5E.Normal"),
- callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
- },
- disadvantage: {
- label: game.i18n.localize("SW5E.Disadvantage"),
- callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
- }
+ default: defaultButton,
+ close: () => resolve(null)
},
- default: defaultButton,
- close: () => resolve(null)
- }, options).render(true);
+ options
+ ).render(true);
});
}
@@ -195,16 +208,16 @@ export default class D20Roll extends Roll {
const form = html[0].querySelector("form");
// Append a situational bonus term
- if ( form.bonus.value ) {
+ if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
- if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
+ if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Customize the modifier
- if ( form.ability?.value ) {
+ if (form.ability?.value) {
const abl = this.data.abilities[form.ability.value];
- this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod}));
+ this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
}
diff --git a/module/dice/damage-roll.js b/module/dice/damage-roll.js
index cc901fb2..41545c8b 100644
--- a/module/dice/damage-roll.js
+++ b/module/dice/damage-roll.js
@@ -13,7 +13,7 @@ export default class DamageRoll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
// For backwards compatibility, skip rolls which do not have the "critical" option defined
- if ( this.options.critical !== undefined ) this.configureDamage();
+ if (this.options.critical !== undefined) this.configureDamage();
}
/**
@@ -42,44 +42,44 @@ export default class DamageRoll extends Roll {
*/
configureDamage() {
let flatBonus = 0;
- for ( let [i, term] of this.terms.entries() ) {
-
+ for (let [i, term] of this.terms.entries()) {
// Multiply dice terms
- if ( term instanceof DiceTerm ) {
+ if (term instanceof DiceTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
- if ( this.isCritical ) {
+ if (this.isCritical) {
let cm = this.options.criticalMultiplier ?? 2;
// Powerful critical - maximize damage and reduce the multiplier by 1
- if ( this.options.powerfulCritical ) {
- flatBonus += (term.number * term.faces);
- cm = Math.max(1, cm-1);
+ if (this.options.powerfulCritical) {
+ flatBonus += term.number * term.faces;
+ cm = Math.max(1, cm - 1);
}
// Alter the damage term
- let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0;
+ let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
term.alter(cm, cb);
term.options.critical = true;
}
-
}
// Multiply numeric terms
- else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) {
+ else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
- if ( this.isCritical ) {
- term.number *= (this.options.criticalMultiplier ?? 2);
+ if (this.isCritical) {
+ term.number *= this.options.criticalMultiplier ?? 2;
term.options.critical = true;
}
}
}
// Add powerful critical bonus
- if ( this.options.powerfulCritical && (flatBonus > 0) ) {
+ if (this.options.powerfulCritical && flatBonus > 0) {
this.terms.push(new OperatorTerm({operator: "+"}));
- this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")}));
+ this.terms.push(
+ new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
+ );
}
// Re-compile the underlying formula
@@ -89,9 +89,9 @@ export default class DamageRoll extends Roll {
/* -------------------------------------------- */
/** @inheritdoc */
- toMessage(messageData={}, options={}) {
+ toMessage(messageData = {}, options = {}) {
messageData.flavor = messageData.flavor || this.options.flavor;
- if ( this.isCritical ) {
+ if (this.isCritical) {
const label = game.i18n.localize("SW5E.CriticalHit");
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
}
@@ -114,34 +114,39 @@ export default class DamageRoll extends Roll {
* @param {object} options Additional Dialog customization options
* @returns {Promise} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
- async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) {
-
+ async configureDialog(
+ {title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
+ options = {}
+ ) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
- rollModes: CONFIG.Dice.rollModes,
+ rollModes: CONFIG.Dice.rollModes
});
// Create the Dialog window and await submission of the form
- return new Promise(resolve => {
- new Dialog({
- title,
- content,
- buttons: {
- critical: {
- condition: allowCritical,
- label: game.i18n.localize("SW5E.CriticalHit"),
- callback: html => resolve(this._onDialogSubmit(html, true))
+ return new Promise((resolve) => {
+ new Dialog(
+ {
+ title,
+ content,
+ buttons: {
+ critical: {
+ condition: allowCritical,
+ label: game.i18n.localize("SW5E.CriticalHit"),
+ callback: (html) => resolve(this._onDialogSubmit(html, true))
+ },
+ normal: {
+ label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
+ callback: (html) => resolve(this._onDialogSubmit(html, false))
+ }
},
- normal: {
- label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
- callback: html => resolve(this._onDialogSubmit(html, false))
- }
+ default: defaultCritical ? "critical" : "normal",
+ close: () => resolve(null)
},
- default: defaultCritical ? "critical" : "normal",
- close: () => resolve(null)
- }, options).render(true);
+ options
+ ).render(true);
});
}
@@ -157,9 +162,9 @@ export default class DamageRoll extends Roll {
const form = html[0].querySelector("form");
// Append a situational bonus term
- if ( form.bonus.value ) {
+ if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
- if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
+ if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
diff --git a/module/effects.js b/module/effects.js
index ef66ef77..b825af87 100644
--- a/module/effects.js
+++ b/module/effects.js
@@ -4,26 +4,28 @@
* @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 owner.createEmbeddedDocuments("ActiveEffect", [{
- label: game.i18n.localize("SW5E.EffectNew"),
- icon: "icons/svg/aura.svg",
- origin: owner.uuid,
- "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
- disabled: li.dataset.effectType === "inactive"
- }]);
- case "edit":
- return effect.sheet.render(true);
- case "delete":
- return effect.delete();
- case "toggle":
- return effect.update({disabled: !effect.data.disabled});
- }
+ 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 owner.createEmbeddedDocuments("ActiveEffect", [
+ {
+ "label": game.i18n.localize("SW5E.EffectNew"),
+ "icon": "icons/svg/aura.svg",
+ "origin": owner.uuid,
+ "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
+ "disabled": li.dataset.effectType === "inactive"
+ }
+ ]);
+ case "edit":
+ return effect.sheet.render(true);
+ case "delete":
+ return effect.delete();
+ case "toggle":
+ return effect.update({disabled: !effect.data.disabled});
+ }
}
/**
@@ -32,32 +34,31 @@ export function onManageActiveEffect(event, owner) {
* @return {object} Data for rendering
*/
export function prepareActiveEffectCategories(effects) {
-
// Define effect header categories
const categories = {
- temporary: {
- type: "temporary",
- label: game.i18n.localize("SW5E.EffectTemporary"),
- effects: []
- },
- passive: {
- type: "passive",
- label: game.i18n.localize("SW5E.EffectPassive"),
- effects: []
- },
- inactive: {
- type: "inactive",
- label: game.i18n.localize("SW5E.EffectInactive"),
- effects: []
- }
+ temporary: {
+ type: "temporary",
+ label: game.i18n.localize("SW5E.EffectTemporary"),
+ effects: []
+ },
+ passive: {
+ type: "passive",
+ label: game.i18n.localize("SW5E.EffectPassive"),
+ effects: []
+ },
+ inactive: {
+ type: "inactive",
+ label: game.i18n.localize("SW5E.EffectInactive"),
+ 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);
+ 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;
-}
\ No newline at end of file
+}
diff --git a/module/item/entity.js b/module/item/entity.js
index 11ffc620..37784699 100644
--- a/module/item/entity.js
+++ b/module/item/entity.js
@@ -6,306 +6,313 @@ import AbilityUseDialog from "../apps/ability-use-dialog.js";
* @extends {Item}
*/
export default class Item5e extends Item {
+ /* -------------------------------------------- */
+ /* Item Properties */
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
- /* Item Properties */
- /* -------------------------------------------- */
+ /**
+ * Determine which ability score modifier is used by this item
+ * @type {string|null}
+ */
+ get abilityMod() {
+ const itemData = this.data.data;
+ if (!("ability" in itemData)) return null;
- /**
- * Determine which ability score modifier is used by this item
- * @type {string|null}
- */
- get abilityMod() {
- const itemData = this.data.data;
- if (!("ability" in itemData)) return null;
+ // Case 1 - defined directly by the item
+ if (itemData.ability) return itemData.ability;
+ // Case 2 - inferred from a parent actor
+ else if (this.actor) {
+ const actorData = this.actor.data.data;
- // Case 1 - defined directly by the item
- if (itemData.ability) return itemData.ability;
+ // Powers - Use Actor powercasting modifier based on power school
+ if (this.data.type === "power") {
+ switch (this.data.data.school) {
+ case "lgt":
+ return "wis";
+ case "uni":
+ return actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod ? "wis" : "cha";
+ case "drk":
+ return "cha";
+ case "tec":
+ return "int";
+ }
+ return "none";
+ }
- // Case 2 - inferred from a parent actor
- else if (this.actor) {
- const actorData = this.actor.data.data;
+ // Tools - default to Intelligence
+ else if (this.data.type === "tool") return "int";
+ // Weapons
+ else if (this.data.type === "weapon") {
+ const wt = itemData.weaponType;
- // Powers - Use Actor powercasting modifier based on power school
- if (this.data.type === "power") {
- switch (this.data.data.school) {
- case "lgt": return "wis";
- case "uni": return (actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod) ? "wis" : "cha";
- case "drk": return "cha";
- case "tec": return "int";
- }
- return "none";
- }
-
+ // Weapons using the powercasting modifier
+ // No current SW5e weapons use this, but it's worth checking just in case
+ if (["mpak", "rpak"].includes(itemData.actionType)) {
+ return actorData.attributes.powercasting || "int";
+ }
- // Tools - default to Intelligence
- else if (this.data.type === "tool") return "int";
+ // Finesse weapons - Str or Dex (PHB pg. 147)
+ else if (itemData.properties.fin === true) {
+ return actorData.abilities["dex"].mod >= actorData.abilities["str"].mod ? "dex" : "str";
+ }
- // Weapons
- else if (this.data.type === "weapon") {
- const wt = itemData.weaponType;
-
- // Weapons using the powercasting modifier
- // No current SW5e weapons use this, but it's worth checking just in case
- if (["mpak", "rpak"].includes(itemData.actionType)) {
- return actorData.attributes.powercasting || "int";
+ // Ranged weapons - Dex (PH p.194)
+ else if (["simpleB", "martialB"].includes(wt)) return "dex";
+ }
+ return "str";
}
- // Finesse weapons - Str or Dex (PHB pg. 147)
- else if (itemData.properties.fin === true) {
- return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str";
+ // Case 3 - unknown
+ return null;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the Item implement an attack roll as part of its usage
+ * @type {boolean}
+ */
+ get hasAttack() {
+ return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the Item implement a damage roll as part of its usage
+ * @type {boolean}
+ */
+ get hasDamage() {
+ return !!(this.data.data.damage && this.data.data.damage.parts.length);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the Item implement a versatile damage roll as part of its usage
+ * @type {boolean}
+ */
+ get isVersatile() {
+ return !!(this.hasDamage && this.data.data.damage.versatile);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the item provide an amount of healing instead of conventional damage?
+ * @return {boolean}
+ */
+ get isHealing() {
+ return this.data.data.actionType === "heal" && this.data.data.damage.parts.length;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the Item implement a saving throw as part of its usage
+ * @type {boolean}
+ */
+ get hasSave() {
+ const save = this.data.data?.save || {};
+ return !!(save.ability && save.scaling);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the Item have a target
+ * @type {boolean}
+ */
+ get hasTarget() {
+ const target = this.data.data.target;
+ return target && !["none", ""].includes(target.type);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Does the Item have an area of effect target
+ * @type {boolean}
+ */
+ get hasAreaTarget() {
+ const target = this.data.data.target;
+ return target && target.type in CONFIG.SW5E.areaTargetTypes;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * A flag for whether this Item is limited in it's ability to be used by charges or by recharge.
+ * @type {boolean}
+ */
+ get hasLimitedUses() {
+ let chg = this.data.data.recharge || {};
+ let uses = this.data.data.uses || {};
+ return !!chg.value || (uses.per && uses.max > 0);
+ }
+
+ /* -------------------------------------------- */
+ /* Data Preparation */
+ /* -------------------------------------------- */
+
+ /**
+ * Augment the basic Item data model with additional dynamic data.
+ */
+ prepareDerivedData() {
+ super.prepareDerivedData();
+
+ // Get the Item's data
+ const itemData = this.data;
+ const data = itemData.data;
+ const C = CONFIG.SW5E;
+ const labels = (this.labels = {});
+
+ // Classes
+ if (itemData.type === "class") {
+ data.levels = Math.clamped(data.levels, 1, 20);
}
- // Ranged weapons - Dex (PH p.194)
- else if ( ["simpleB", "martialB"].includes(wt) ) return "dex";
- }
- return "str";
+ // 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) => {
+ if (c[1] !== true) return arr;
+ arr.push(c[0].titleCase().slice(0, 1));
+ return arr;
+ }, []);
+ labels.materials = data?.materials?.value ?? null;
+ }
+
+ // Feat Items
+ else if (itemData.type === "feat") {
+ const act = data.activation;
+ if (act && act.type === C.abilityActivationTypes.legendary)
+ labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel");
+ else if (act && act.type === C.abilityActivationTypes.lair)
+ labels.featType = game.i18n.localize("SW5E.LairActionLabel");
+ else if (act && act.type)
+ labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action");
+ else labels.featType = game.i18n.localize("SW5E.Passive");
+ }
+
+ // TODO: Something with all this
+ // 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") {
+ // labels.background = C.background[data.background];
+ }
+ // Class Feature Items
+ else if (itemData.type === "classfeature") {
+ // labels.classFeature = C.classFeature[data.classFeature];
+ }
+ // Deployment Items
+ else if (itemData.type === "deployment") {
+ // labels.deployment = C.deployment[data.deployment];
+ }
+ // Venture Items
+ else if (itemData.type === "venture") {
+ // labels.venture = C.venture[data.venture];
+ }
+ // 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
+ else if (itemData.type === "equipment") {
+ labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : "";
+ }
+
+ // Activated Items
+ if (data.hasOwnProperty("activation")) {
+ // Ability Activation Label
+ let act = data.activation || {};
+ if (act) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" ");
+
+ // Target Label
+ let tgt = data.target || {};
+ if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null;
+ if (["none", "self"].includes(tgt.type)) {
+ tgt.value = null;
+ tgt.units = null;
+ }
+ labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" ");
+
+ // Range Label
+ let rng = data.range || {};
+ if (["none", "touch", "self"].includes(rng.units)) {
+ rng.value = null;
+ rng.long = null;
+ }
+ labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" ");
+
+ // Duration Label
+ let dur = data.duration || {};
+ if (["inst", "perm"].includes(dur.units)) dur.value = null;
+ labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" ");
+
+ // Recharge Label
+ let chg = data.recharge || {};
+ labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${
+ parseInt(chg.value) < 6 ? "+" : ""
+ }]`;
+ }
+
+ // Item Actions
+ if (data.hasOwnProperty("actionType")) {
+ // Damage
+ let dam = data.damage || {};
+ if (dam.parts) {
+ labels.damage = dam.parts
+ .map((d) => d[0])
+ .join(" + ")
+ .replace(/\+ -/g, "- ");
+ labels.damageTypes = dam.parts.map((d) => C.damageTypes[d[1]]).join(", ");
+ }
+ }
+
+ // if this item is owned, we prepareFinalAttributes() at the end of actor init
+ if (!this.isOwned) this.prepareFinalAttributes();
}
- // Case 3 - unknown
- return null
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Compute item attributes which might depend on prepared actor data.
+ */
+ prepareFinalAttributes() {
+ if (this.data.data.hasOwnProperty("actionType")) {
+ // Saving throws
+ this.getSaveDC();
- /**
- * Does the Item implement an attack roll as part of its usage
- * @type {boolean}
- */
- get hasAttack() {
- return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType);
- }
+ // To Hit
+ this.getAttackToHit();
- /* -------------------------------------------- */
+ // Limited Uses
+ this.prepareMaxUses();
- /**
- * Does the Item implement a damage roll as part of its usage
- * @type {boolean}
- */
- get hasDamage() {
- return !!(this.data.data.damage && this.data.data.damage.parts.length);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Does the Item implement a versatile damage roll as part of its usage
- * @type {boolean}
- */
- get isVersatile() {
- return !!(this.hasDamage && this.data.data.damage.versatile);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Does the item provide an amount of healing instead of conventional damage?
- * @return {boolean}
- */
- get isHealing() {
- return (this.data.data.actionType === "heal") && this.data.data.damage.parts.length;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Does the Item implement a saving throw as part of its usage
- * @type {boolean}
- */
- get hasSave() {
- const save = this.data.data?.save || {};
- return !!(save.ability && save.scaling);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Does the Item have a target
- * @type {boolean}
- */
- get hasTarget() {
- const target = this.data.data.target;
- return target && !["none",""].includes(target.type);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Does the Item have an area of effect target
- * @type {boolean}
- */
- get hasAreaTarget() {
- const target = this.data.data.target;
- return target && (target.type in CONFIG.SW5E.areaTargetTypes);
- }
-
- /* -------------------------------------------- */
-
- /**
- * A flag for whether this Item is limited in it's ability to be used by charges or by recharge.
- * @type {boolean}
- */
- get hasLimitedUses() {
- let chg = this.data.data.recharge || {};
- let uses = this.data.data.uses || {};
- return !!chg.value || (uses.per && (uses.max > 0));
- }
-
- /* -------------------------------------------- */
- /* Data Preparation */
- /* -------------------------------------------- */
-
- /**
- * Augment the basic Item data model with additional dynamic data.
- */
- prepareDerivedData() {
- super.prepareDerivedData();
-
- // Get the Item's data
- const itemData = this.data;
- const data = itemData.data;
- const C = CONFIG.SW5E;
- const labels = this.labels = {};
-
- // Classes
- if ( itemData.type === "class" ) {
- data.levels = Math.clamped(data.levels, 1, 20);
+ // Damage Label
+ this.getDerivedDamageLabel();
+ }
}
- // 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) => {
- if ( c[1] !== true ) return arr;
- arr.push(c[0].titleCase().slice(0, 1));
- return arr;
- }, []);
- labels.materials = data?.materials?.value ?? null;
- }
-
- // Feat Items
- else if ( itemData.type === "feat" ) {
- const act = data.activation;
- if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel");
- else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = game.i18n.localize("SW5E.LairActionLabel");
- else if ( act && act.type ) labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action");
- else labels.featType = game.i18n.localize("SW5E.Passive");
- }
-
- // TODO: Something with all this
- // 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" ) {
- // labels.background = C.background[data.background];
- }
- // Class Feature Items
- else if ( itemData.type === "classfeature" ) {
- // labels.classFeature = C.classFeature[data.classFeature];
- }
- // Deployment Items
- else if ( itemData.type === "deployment" ) {
- // labels.deployment = C.deployment[data.deployment];
- }
- // Venture Items
- else if ( itemData.type === "venture" ) {
- // labels.venture = C.venture[data.venture];
- }
- // 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
- else if ( itemData.type === "equipment" ) {
- labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : "";
- }
-
- // Activated Items
- if ( data.hasOwnProperty("activation") ) {
-
- // Ability Activation Label
- let act = data.activation || {};
- if ( act ) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" ");
-
- // Target Label
- let tgt = data.target || {};
- if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null;
- if (["none", "self"].includes(tgt.type)) {
- tgt.value = null;
- tgt.units = null;
- }
- labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" ");
-
- // Range Label
- let rng = data.range || {};
- if ( ["none", "touch", "self"].includes(rng.units) ) {
- rng.value = null;
- rng.long = null;
- }
- labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" ");
-
- // Duration Label
- let dur = data.duration || {};
- if (["inst", "perm"].includes(dur.units)) dur.value = null;
- labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" ");
-
- // Recharge Label
- let chg = data.recharge || {};
- labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`;
- }
-
- // Item Actions
- if ( data.hasOwnProperty("actionType") ) {
- // Damage
- let dam = data.damage || {};
- if (dam.parts) {
- labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
- labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", ");
- }
- }
-
- // if this item is owned, we prepareFinalAttributes() at the end of actor init
- if (!this.isOwned) this.prepareFinalAttributes();
- }
-
- /* -------------------------------------------- */
-
- /**
- * Compute item attributes which might depend on prepared actor data.
- */
- prepareFinalAttributes() {
- if ( this.data.data.hasOwnProperty("actionType") ) {
- // Saving throws
- this.getSaveDC();
-
- // To Hit
- this.getAttackToHit();
-
- // Limited Uses
- this.prepareMaxUses();
-
- // Damage Label
- this.getDerivedDamageLabel();
- }
- }
-
/* -------------------------------------------- */
/**
@@ -316,1321 +323,1349 @@ export default class Item5e extends Item {
* @returns {Array} array of objects with `formula` and `damageType`
*/
getDerivedDamageLabel() {
- const itemData = this.data.data;
- if ( !this.hasDamage || !itemData || !this.isOwned ) return [];
+ const itemData = this.data.data;
+ if (!this.hasDamage || !itemData || !this.isOwned) return [];
- const rollData = this.getRollData();
+ const rollData = this.getRollData();
- const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({
- formula: simplifyRollFormula(damagePart[0], rollData, { constantFirst: false }),
- damageType: damagePart[1],
- }));
+ const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({
+ formula: simplifyRollFormula(damagePart[0], rollData, {constantFirst: false}),
+ damageType: damagePart[1]
+ }));
- this.labels.derivedDamage = derivedDamage
+ this.labels.derivedDamage = derivedDamage;
- return derivedDamage;
+ return derivedDamage;
}
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Update the derived power DC for an item that requires a saving throw
- * @returns {number|null}
- */
- getSaveDC() {
- if ( !this.hasSave ) return;
- const save = this.data.data?.save;
+ /**
+ * Update the derived power DC for an item that requires a saving throw
+ * @returns {number|null}
+ */
+ getSaveDC() {
+ if (!this.hasSave) return;
+ const save = this.data.data?.save;
- // Actor power-DC based scaling
- if ( save.scaling === "power" ) {
- switch (this.data.data.school) {
- case "lgt": {
- save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null;
- break;
- }
- case "uni": {
- save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null;
- break;
- }
- case "drk": {
- save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null;
- break;
- }
- case "tec": {
- save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null;
- break;
- }
- }
- }
-
- // Ability-score based scaling
- else if ( save.scaling !== "flat" ) {
- save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null;
- }
-
- // Update labels
- const abl = CONFIG.SW5E.abilities[save.ability];
- this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl});
- return save.dc;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Update a label to the Item detailing its total to hit bonus.
- * Sources:
- * - item entity's innate attack bonus
- * - item's actor's proficiency bonus if applicable
- * - item's actor's global bonuses to the given item type
- * - item's ammunition if applicable
- *
- * @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll
- */
- getAttackToHit() {
- const itemData = this.data.data;
- if ( !this.hasAttack || !itemData ) return;
- const rollData = this.getRollData();
-
- // Define Roll bonuses
- const parts = [];
-
- // Include the item's innate attack bonus as the initial value and label
- if ( itemData.attackBonus ) {
- parts.push(itemData.attackBonus)
- this.labels.toHit = itemData.attackBonus;
- }
-
- // Take no further action for un-owned items
- if ( !this.isOwned ) return {rollData, parts};
-
- // Ability score modifier
- parts.push(`@mod`);
-
- // Add proficiency bonus if an explicit proficiency flag is present or for non-item features
- if ( !["weapon", "consumable"].includes(this.data.type) || itemData.proficient ) {
- parts.push("@prof");
- }
-
- // Actor-level global bonus to attack rolls
- const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {};
- if ( actorBonus.attack ) parts.push(actorBonus.attack);
-
- // One-time bonus provided by consumed ammunition
- if ( (itemData.consume?.type === 'ammo') && !!this.actor.items ) {
- const ammoItemData = this.actor.items.get(itemData.consume.target)?.data;
-
- if (ammoItemData) {
- const ammoItemQuantity = ammoItemData.data.quantity;
- const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0);
- const ammoItemAttackBonus = ammoItemData.data.attackBonus;
- const ammoIsTypeConsumable = (ammoItemData.type === "consumable") && (ammoItemData.data.consumableType === "ammo")
- if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) {
- parts.push("@ammo");
- rollData["ammo"] = ammoItemAttackBonus;
- }
- }
- }
-
- // Condense the resulting attack bonus formula into a simplified label
- let toHitLabel = simplifyRollFormula(parts.join('+'), rollData).trim();
- if (toHitLabel.charAt(0) !== '-') {
- toHitLabel = '+ ' + toHitLabel
- }
- this.labels.toHit = toHitLabel;
-
- // Update labels and return the prepared roll data
- return {rollData, parts};
- }
-
- /* -------------------------------------------- */
-
- /**
- * Populates the max uses of an item.
- * If the item is an owned item and the `max` is not numeric, calculate based on actor data.
- */
- prepareMaxUses() {
- const data = this.data.data;
- if (!data.uses?.max) return;
- let max = data.uses.max;
-
- // if this is an owned item and the max is not numeric, we need to calculate it
- if (this.isOwned && !Number.isNumeric(max)) {
- if (this.actor.data === undefined) return;
- try {
- max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true});
- max = Roll.safeEval(max);
- } catch(e) {
- console.error('Problem preparing Max uses for', this.data.name, e);
- return;
- }
- }
- data.uses.max = Number(max);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options
- * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable?
- * @param {string} [rollMode] The roll display mode with which to display (or not) the card
- * @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return
- * the prepared chat message data (if false).
- * @return {Promise}
- */
- async roll({configureDialog=true, rollMode, createMessage=true}={}) {
- let item = this;
- const id = this.data.data; // Item system data
- const actor = this.actor;
- const ad = actor.data.data; // Actor system data
-
- // Reference aspects of the item data necessary for usage
- const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
- const resource = id.consume || {}; // Resource consumption
- const recharge = id.recharge || {}; // Recharge mechanic
- const uses = id?.uses ?? {}; // Limited uses
- const isPower = this.type === "power"; // Does the item require a power slot?
- // TODO: Possibly Mod this to not consume slots based on class?
- // We could use this for feats and architypes that let a character cast one slot every rest or so
- const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
-
- // Define follow-up actions resulting from the item usage
- let createMeasuredTemplate = hasArea; // Trigger a template creation
- let consumeRecharge = !!recharge.value; // Consume recharge
- let consumeResource = !!resource.target && resource.type !== "ammo"; // Consume a linked (non-ammo) resource
- let consumePowerSlot = requirePowerSlot; // Consume a power slot
- let consumeUsage = !!uses.per; // Consume limited uses
- let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
- let consumePowerLevel = null; // Consume a specific category of power slot
- if ( requirePowerSlot ) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`;
-
- // Display a configuration dialog to customize the usage
- const needsConfiguration = createMeasuredTemplate || consumeRecharge || (consumeResource && !['simpleB', 'martialB'].includes(id.weaponType)) || consumePowerSlot || (consumeUsage && !['simpleB', 'martialB'].includes(id.weaponType));
- if (configureDialog && needsConfiguration) {
- const configuration = await AbilityUseDialog.create(this);
- if (!configuration) return;
-
-
- // Determine consumption preferences
- createMeasuredTemplate = Boolean(configuration.placeTemplate);
- consumeUsage = Boolean(configuration.consumeUse);
- consumeRecharge = Boolean(configuration.consumeRecharge);
- consumeResource = Boolean(configuration.consumeResource);
- consumePowerSlot = Boolean(configuration.consumeSlot);
-
- // Handle power upcasting
- if ( requirePowerSlot ) {
- consumePowerLevel = `power${configuration.level}`;
- if (consumePowerSlot === false) consumePowerLevel = null;
- const upcastLevel = parseInt(configuration.level);
- if (upcastLevel !== id.level) {
- item = this.clone({"data.level": upcastLevel}, {keepId: true});
- item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+)
- item.prepareFinalAttributes(); // Power save DC, etc...
- }
- }
- }
-
- // Determine whether the item can be used by testing for resource consumption
- const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerLevel, consumeUsage, consumeQuantity});
- if ( !usage ) return;
-
- const {actorUpdates, itemUpdates, resourceUpdates} = usage;
-
- // Commit pending data updates
- if ( !foundry.utils.isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
- if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete();
- if ( !foundry.utils.isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
- if ( !foundry.utils.isObjectEmpty(resourceUpdates) ) {
- const resource = actor.items.get(id.consume?.target);
- if ( resource ) await resource.update(resourceUpdates);
- }
-
- // Initiate measured template creation
- if ( createMeasuredTemplate ) {
- const template = game.sw5e.canvas.AbilityTemplate.fromItem(item);
- if ( template ) template.drawPreview();
- }
-
- // Create or return the Chat Message data
- return item.displayCard({rollMode, createMessage});
- }
-
- /* -------------------------------------------- */
-
- /**
- * Verify that the consumed resources used by an Item are available.
- * Otherwise display an error and return false.
- * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
- * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
- * @param {boolean} consumeResource Whether the item consumes a limited resource
- * @param {string|null} consumePowerLevel The category of power slot to consume, or null
- * @param {boolean} consumeUsage Whether the item consumes a limited usage
- * @returns {object|boolean} A set of data changes to apply when the item is used, or false
- * @private
- */
- _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) {
-
- // Reference item data
- const id = this.data.data;
- const actorUpdates = {};
- const itemUpdates = {};
- const resourceUpdates = {};
-
- // Consume Recharge
- if ( consumeRecharge ) {
- const recharge = id.recharge || {};
- if ( recharge.charged === false ) {
- ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
- return false;
- }
- itemUpdates["data.recharge.charged"] = false;
- }
-
- // Consume Limited Resource
- if ( consumeResource ) {
- const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates);
- if ( canConsume === false ) return false;
- }
-
- // Consume Power Slots and Force/Tech Points
- if ( consumePowerLevel ) {
- if ( Number.isNumeric(consumePowerLevel) ) consumePowerLevel = `power${consumePowerLevel}`;
- const level = this.actor?.data.data.powers[consumePowerLevel];
- const fp = this.actor.data.data.attributes.force.points;
- const tp = this.actor.data.data.attributes.tech.points;
- const powerCost = id.level + 1;
- const innatePower = this.actor.data.data.attributes.powercasting === 'innate';
- if (!innatePower){
- switch (id.school){
- case "lgt":
- case "uni":
- case "drk": {
- const powers = Number(level?.fvalue ?? 0);
- if ( powers === 0 ) {
- const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
- ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
- return false;
+ // Actor power-DC based scaling
+ if (save.scaling === "power") {
+ switch (this.data.data.school) {
+ case "lgt": {
+ save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null;
+ break;
+ }
+ case "uni": {
+ save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null;
+ break;
+ }
+ case "drk": {
+ save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null;
+ break;
+ }
+ case "tec": {
+ save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null;
+ break;
+ }
}
- actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0);
- if (fp.temp >= powerCost) {
- actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost;
- }else{
- actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost;
- actorUpdates["data.attributes.force.points.temp"] = 0;
+ }
+
+ // Ability-score based scaling
+ else if (save.scaling !== "flat") {
+ save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null;
+ }
+
+ // Update labels
+ const abl = CONFIG.SW5E.abilities[save.ability];
+ this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl});
+ return save.dc;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Update a label to the Item detailing its total to hit bonus.
+ * Sources:
+ * - item entity's innate attack bonus
+ * - item's actor's proficiency bonus if applicable
+ * - item's actor's global bonuses to the given item type
+ * - item's ammunition if applicable
+ *
+ * @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll
+ */
+ getAttackToHit() {
+ const itemData = this.data.data;
+ if (!this.hasAttack || !itemData) return;
+ const rollData = this.getRollData();
+
+ // Define Roll bonuses
+ const parts = [];
+
+ // Include the item's innate attack bonus as the initial value and label
+ if (itemData.attackBonus) {
+ parts.push(itemData.attackBonus);
+ this.labels.toHit = itemData.attackBonus;
+ }
+
+ // Take no further action for un-owned items
+ if (!this.isOwned) return {rollData, parts};
+
+ // Ability score modifier
+ parts.push(`@mod`);
+
+ // Add proficiency bonus if an explicit proficiency flag is present or for non-item features
+ if (!["weapon", "consumable"].includes(this.data.type) || itemData.proficient) {
+ parts.push("@prof");
+ }
+
+ // Actor-level global bonus to attack rolls
+ const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {};
+ if (actorBonus.attack) parts.push(actorBonus.attack);
+
+ // One-time bonus provided by consumed ammunition
+ if (itemData.consume?.type === "ammo" && !!this.actor.items) {
+ const ammoItemData = this.actor.items.get(itemData.consume.target)?.data;
+
+ if (ammoItemData) {
+ const ammoItemQuantity = ammoItemData.data.quantity;
+ const ammoCanBeConsumed = ammoItemQuantity && ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0;
+ const ammoItemAttackBonus = ammoItemData.data.attackBonus;
+ const ammoIsTypeConsumable =
+ ammoItemData.type === "consumable" && ammoItemData.data.consumableType === "ammo";
+ if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) {
+ parts.push("@ammo");
+ rollData["ammo"] = ammoItemAttackBonus;
+ }
}
- break;
- }
- case "tec": {
- const powers = Number(level?.tvalue ?? 0);
- if ( powers === 0 ) {
- const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
- ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
- return false;
+ }
+
+ // Condense the resulting attack bonus formula into a simplified label
+ let toHitLabel = simplifyRollFormula(parts.join("+"), rollData).trim();
+ if (toHitLabel.charAt(0) !== "-") {
+ toHitLabel = "+ " + toHitLabel;
+ }
+ this.labels.toHit = toHitLabel;
+
+ // Update labels and return the prepared roll data
+ return {rollData, parts};
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Populates the max uses of an item.
+ * If the item is an owned item and the `max` is not numeric, calculate based on actor data.
+ */
+ prepareMaxUses() {
+ const data = this.data.data;
+ if (!data.uses?.max) return;
+ let max = data.uses.max;
+
+ // if this is an owned item and the max is not numeric, we need to calculate it
+ if (this.isOwned && !Number.isNumeric(max)) {
+ if (this.actor.data === undefined) return;
+ try {
+ max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true});
+ max = Roll.safeEval(max);
+ } catch (e) {
+ console.error("Problem preparing Max uses for", this.data.name, e);
+ return;
}
- actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0);
- if (tp.temp >= powerCost) {
- actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost;
- }else{
- actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost;
- actorUpdates["data.attributes.tech.points.temp"] = 0;
+ }
+ data.uses.max = Number(max);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options
+ * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable?
+ * @param {string} [rollMode] The roll display mode with which to display (or not) the card
+ * @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return
+ * the prepared chat message data (if false).
+ * @return {Promise}
+ */
+ async roll({configureDialog = true, rollMode, createMessage = true} = {}) {
+ let item = this;
+ const id = this.data.data; // Item system data
+ const actor = this.actor;
+ const ad = actor.data.data; // Actor system data
+
+ // Reference aspects of the item data necessary for usage
+ const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
+ const resource = id.consume || {}; // Resource consumption
+ const recharge = id.recharge || {}; // Recharge mechanic
+ const uses = id?.uses ?? {}; // Limited uses
+ const isPower = this.type === "power"; // Does the item require a power slot?
+ // TODO: Possibly Mod this to not consume slots based on class?
+ // We could use this for feats and architypes that let a character cast one slot every rest or so
+ const requirePowerSlot = isPower && id.level > 0 && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
+
+ // Define follow-up actions resulting from the item usage
+ let createMeasuredTemplate = hasArea; // Trigger a template creation
+ let consumeRecharge = !!recharge.value; // Consume recharge
+ let consumeResource = !!resource.target && resource.type !== "ammo"; // Consume a linked (non-ammo) resource
+ let consumePowerSlot = requirePowerSlot; // Consume a power slot
+ let consumeUsage = !!uses.per; // Consume limited uses
+ let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
+ let consumePowerLevel = null; // Consume a specific category of power slot
+ if (requirePowerSlot) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`;
+
+ // Display a configuration dialog to customize the usage
+ const needsConfiguration =
+ createMeasuredTemplate ||
+ consumeRecharge ||
+ (consumeResource && !["simpleB", "martialB"].includes(id.weaponType)) ||
+ consumePowerSlot ||
+ (consumeUsage && !["simpleB", "martialB"].includes(id.weaponType));
+ if (configureDialog && needsConfiguration) {
+ const configuration = await AbilityUseDialog.create(this);
+ if (!configuration) return;
+
+ // Determine consumption preferences
+ createMeasuredTemplate = Boolean(configuration.placeTemplate);
+ consumeUsage = Boolean(configuration.consumeUse);
+ consumeRecharge = Boolean(configuration.consumeRecharge);
+ consumeResource = Boolean(configuration.consumeResource);
+ consumePowerSlot = Boolean(configuration.consumeSlot);
+
+ // Handle power upcasting
+ if (requirePowerSlot) {
+ consumePowerLevel = `power${configuration.level}`;
+ if (consumePowerSlot === false) consumePowerLevel = null;
+ const upcastLevel = parseInt(configuration.level);
+ if (upcastLevel !== id.level) {
+ item = this.clone({"data.level": upcastLevel}, {keepId: true});
+ item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+)
+ item.prepareFinalAttributes(); // Power save DC, etc...
+ }
}
- break;
- }
}
- }
- }
-
- // Consume Limited Usage
- if ( consumeUsage ) {
- const uses = id.uses || {};
- const available = Number(uses.value ?? 0);
- let used = false;
-
- // Reduce usages
- const remaining = Math.max(available - 1, 0);
- if ( available >= 1 ) {
- used = true;
- itemUpdates["data.uses.value"] = remaining;
- }
-
- // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity
- if ( consumeQuantity && (!used || (remaining === 0)) ) {
- const q = Number(id.quantity ?? 1);
- if ( q >= 1 ) {
- used = true;
- itemUpdates["data.quantity"] = Math.max(q - 1, 0);
- itemUpdates["data.uses.value"] = uses.max ?? 1;
- }
- }
-
- // If the item was not used, return a warning
- if ( !used ) {
- ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
- return false;
- }
- }
-
- // Return the configured usage
- return {itemUpdates, actorUpdates, resourceUpdates};
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle update actions required when consuming an external resource
- * @param {object} itemUpdates An object of data updates applied to this item
- * @param {object} actorUpdates An object of data updates applied to the item owner (Actor)
- * @param {object} resourceUpdates An object of data updates applied to a different resource item (Item)
- * @return {boolean|void} Return false to block further progress, or return nothing to continue
- * @private
- */
- _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) {
- const actor = this.actor;
- const itemData = this.data.data;
- const consume = itemData.consume || {};
- if ( !consume.type ) return;
-
- // No consumed target
- const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
- if ( !consume.target ) {
- ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
- return false;
- }
-
- // Identify the consumed resource and its current quantity
- let resource = null;
- let amount = Number(consume.amount ?? 1);
- let quantity = 0;
- switch ( consume.type ) {
- case "attribute":
- resource = getProperty(actor.data.data, consume.target);
- quantity = resource || 0;
- break;
- case "ammo":
- case "material":
- resource = actor.items.get(consume.target);
- quantity = resource ? resource.data.data.quantity : 0;
- break;
- case "charges":
- resource = actor.items.get(consume.target);
- if ( !resource ) break;
- const uses = resource.data.data.uses;
- if ( uses.per && uses.max ) quantity = uses.value;
- else if ( resource.data.data.recharge?.value ) {
- quantity = resource.data.data.recharge.charged ? 1 : 0;
- amount = 1;
- }
- break;
- }
-
- // Verify that a consumed resource is available
- if ( !resource ) {
- ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
- return false;
- }
-
- // Verify that the required quantity is available
- let remaining = quantity - amount;
- if ( remaining < 0 ) {
- ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
- return false;
- }
-
- // Define updates to provided data objects
- switch ( consume.type ) {
- case "attribute":
- actorUpdates[`data.${consume.target}`] = remaining;
- break;
- case "ammo":
- case "material":
- resourceUpdates["data.quantity"] = remaining;
- break;
- case "charges":
- const uses = resource.data.data.uses || {};
- const recharge = resource.data.data.recharge || {};
- if ( uses.per && uses.max ) resourceUpdates["data.uses.value"] = remaining;
- else if ( recharge.value ) resourceUpdates["data.recharge.charged"] = false;
- break;
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Display the chat card for an Item as a Chat Message
- * @param {object} options Options which configure the display of the item chat card
- * @param {string} rollMode The message visibility mode to apply to the created card
- * @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return
- * the prepared message data (if false)
- */
- async displayCard({rollMode, createMessage=true}={}) {
-
- // Render the chat card template
- const token = this.actor.token;
- const templateData = {
- actor: this.actor,
- tokenId: token?.uuid || null,
- item: this.data,
- data: this.getChatData(),
- labels: this.labels,
- hasAttack: this.hasAttack,
- isHealing: this.isHealing,
- hasDamage: this.hasDamage,
- isVersatile: this.isVersatile,
- isPower: this.data.type === "power",
- hasSave: this.hasSave,
- hasAreaTarget: this.hasAreaTarget,
- isTool: this.data.type === "tool"
- };
- const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData);
-
- // Create the ChatMessage data object
- const chatData = {
- user: game.user.data._id,
- type: CONST.CHAT_MESSAGE_TYPES.OTHER,
- content: html,
- flavor: this.data.data.chatFlavor || this.name,
- speaker: ChatMessage.getSpeaker({actor: this.actor, token}),
- flags: {"core.canPopout": true}
- };
-
- // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
- if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) {
- chatData.flags["sw5e.itemData"] = this.data;
- }
-
- // Apply the roll mode to adjust message visibility
- ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode"));
-
- // Create the Chat Message or return its data
- return createMessage ? ChatMessage.create(chatData) : chatData;
- }
-
- /* -------------------------------------------- */
- /* Chat Cards */
- /* -------------------------------------------- */
-
- /**
- * Prepare an object of chat data used to display a card for the Item in the chat log
- * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function
- * @return {Object} An object of chat data to render
- */
- getChatData(htmlOptions={}) {
- const data = foundry.utils.deepClone(this.data.data);
- const labels = this.labels;
-
- // Rich text description
- data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions);
-
- // Item type specific properties
- const props = [];
- const fn = this[`_${this.data.type}ChatData`];
- if ( fn ) fn.bind(this)(data, labels, props);
-
- // Equipment properties
- if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) {
- if ( data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED ) props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED]));
- props.push(
- game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
- game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"),
- );
- }
-
- // Ability activation properties
- if ( data.hasOwnProperty("activation") ) {
- props.push(
- labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""),
- labels.target,
- labels.range,
- labels.duration
- );
- }
-
- // Filter properties and return
- data.properties = props.filter(p => !!p);
- return data;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare chat card data for equipment type items
- * @private
- */
- _equipmentChatData(data, labels, props) {
- props.push(
- CONFIG.SW5E.equipmentTypes[data.armor.type],
- labels.armor || null,
- data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null
- );
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare chat card data for weapon type items
- * @private
- */
- _weaponChatData(data, labels, props) {
- props.push(
- CONFIG.SW5E.weaponTypes[data.weaponType],
- );
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare chat card data for consumable type items
- * @private
- */
- _consumableChatData(data, labels, props) {
- props.push(
- CONFIG.SW5E.consumableTypes[data.consumableType],
- data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges")
- );
- data.hasCharges = data.uses.value >= 0;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare chat card data for tool type items
- * @private
- */
- _toolChatData(data, labels, props) {
- props.push(
- CONFIG.SW5E.abilities[data.ability] || null,
- CONFIG.SW5E.proficiencyLevels[data.proficient || 0]
- );
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare chat card data for loot type items
- * @private
- */
- _lootChatData(data, labels, props) {
- props.push(
- game.i18n.localize("SW5E.ItemTypeLoot"),
- data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null
- );
- }
-
- /* -------------------------------------------- */
-
- /**
- * Render a chat card for Power type data
- * @return {Object}
- * @private
- */
- _powerChatData(data, labels, props) {
- props.push(
- labels.level,
- labels.components + (labels.materials ? ` (${labels.materials})` : "")
- );
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare chat card data for items of the "Feat" type
- * @private
- */
- _featChatData(data, labels, props) {
- props.push(data.requirements);
- }
-
- /* -------------------------------------------- */
- /* Item Rolls - Attack, Damage, Saves, Checks */
- /* -------------------------------------------- */
-
- /**
- * Place an attack roll using an item (weapon, feat, power, or equipment)
- * Rely upon the d20Roll logic for the core implementation
- *
- * @param {object} options Roll options which are configured and provided to the d20Roll function
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- async rollAttack(options={}) {
- const itemData = this.data.data;
- const flags = this.actor.data.flags.sw5e || {};
- if ( !this.hasAttack ) {
- throw new Error("You may not place an Attack Roll with this Item.");
- }
- let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`;
-
- // get the parts and rollData for this item's attack
- const {parts, rollData} = this.getAttackToHit();
-
- // Handle ammunition consumption
- delete this._ammo;
- let ammo = null;
- let ammoUpdate = null;
- const consume = itemData.consume;
- if ( consume?.type === "ammo" ) {
- 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;
- title += ` [${ammo.name}]`;
- }
- }
-
- // Get pending ammunition update
- const usage = this._getUsageUpdates({consumeResource: true});
- if ( usage === false ) return null;
- ammoUpdate = usage.resourceUpdates || {};
- }
-
- // 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,
- top: options.event ? options.event.clientY - 80 : null,
- left: window.innerWidth - 710
- },
- messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id }}
- }, options);
- rollConfig.event = options.event;
-
- // 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
- if ( flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod) ) {
- rollConfig.elvenAccuracy = true;
- }
-
-
- // Apply Halfling Lucky
- if ( flags.halflingLucky ) rollConfig.halflingLucky = true;
-
- // Invoke the d20 roll helper
- const roll = await d20Roll(rollConfig);
- if ( roll === false ) return null;
-
- // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
- if ( ammo && !isObjectEmpty(ammoUpdate) ) await ammo.update(ammoUpdate);
- return roll;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Place a damage roll using an item (weapon, feat, power, or equipment)
- * Rely upon the damageRoll logic for the core implementation.
- * @param {MouseEvent} [event] An event which triggered this roll, if any
- * @param {boolean} [critical] Should damage be rolled as a critical hit?
- * @param {number} [powerLevel] If the item is a power, override the level for damage scaling
- * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula
- * @param {object} [options] Additional options passed to the damageRoll function
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollDamage({critical=false, event=null, powerLevel=null, versatile=false, options={}}={}) {
- if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item.");
- const itemData = this.data.data;
- const actorData = this.actor.data.data;
- 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;
-
- // Configure the damage roll
- const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll");
- const title = `${this.name} - ${actionFlavor}`;
- const rollConfig = {
- actor: this.actor,
- critical: critical ?? event?.altKey ?? false,
- data: rollData,
- event: event,
- fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false,
- parts: parts,
- title: title,
- flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
- speaker: ChatMessage.getSpeaker({actor: this.actor}),
- 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 ) {
- parts[0] = itemData.damage.versatile;
- messageData["flags.sw5e.roll"].versatile = true;
- }
-
- // Scale damage from up-casting powers
- if ( (this.data.type === "power") ) {
- if ( (itemData.scaling.mode === "atwill") ) {
- const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
- this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData);
- }
- else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) {
- const scaling = itemData.scaling.formula;
- this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData);
- }
- }
-
- // Add damage bonus formula
- const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {};
- if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) {
- parts.push(actorBonus.damage);
- }
-
- // Handle ammunition damage
- const ammoData = this._ammo?.data;
-
- // only add the ammunition damage if the ammution is a consumable with type 'ammo'
- if ( this._ammo && (ammoData.type === "consumable") && (ammoData.data.consumableType === "ammo") ) {
- parts.push("@ammo");
- rollData["ammo"] = ammoData.data.damage.parts.map(p => p[0]).join("+");
- 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(mergeObject(rollConfig, options));
- }
-
- /* -------------------------------------------- */
-
- /**
- * Adjust an at-will damage formula to scale it for higher level characters and monsters
- * @private
- */
- _scaleAtWillDamage(parts, scale, level, rollData) {
- const add = Math.floor((level + 1) / 6);
- if ( add === 0 ) return;
- this._scaleDamage(parts, scale || parts.join(" + "), add, rollData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Adjust the power damage formula to scale it for power level up-casting
- * @param {Array} parts The original damage parts
- * @param {number} baseLevel The default power level
- * @param {number} powerLevel The casted power level
- * @param {string} formula The scaling formula
- * @param {object} rollData A data object that should be applied to the scaled damage roll
- * @return {string[]} The scaled roll parts
- * @private
- */
- _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) {
- const upcastLevels = Math.max(powerLevel - baseLevel, 0);
- if ( upcastLevels === 0 ) return parts;
- this._scaleDamage(parts, formula, upcastLevels, rollData);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Scale an array of damage parts according to a provided scaling formula and scaling multiplier
- * @param {string[]} parts Initial roll parts
- * @param {string} scaling A scaling formula
- * @param {number} times A number of times to apply the scaling formula
- * @param {object} rollData A data object that should be applied to the scaled damage roll
- * @return {string[]} The scaled roll parts
- * @private
- */
- _scaleDamage(parts, scaling, times, rollData) {
- if ( times <= 0 ) return parts;
- const p0 = new Roll(parts[0], rollData);
- const s = new Roll(scaling, rollData).alter(times);
-
- // Attempt to simplify by combining like dice terms
- let simplified = false;
- if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) {
- const d0 = p0.terms[0];
- const s0 = s.terms[0];
- if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) {
- d0.number += s0.number;
- parts[0] = p0.formula;
- simplified = true;
- }
- }
-
- // Otherwise add to the first part
- if ( !simplified ) {
- parts[0] = `${parts[0]} + ${s.formula}`;
- }
- return parts;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Place an attack roll using an item (weapon, feat, power, or equipment)
- * Rely upon the d20Roll logic for the core implementation
- *
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- async rollFormula(options={}) {
- if ( !this.data.data.formula ) {
- throw new Error("This Item does not have a formula to roll!");
- }
-
- // Define Roll Data
- const rollData = this.getRollData();
- if ( options.powerLevel ) rollData.item.level = options.powerLevel;
- const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`;
-
- // Invoke the roll and submit it to chat
- const roll = new Roll(rollData.item.formula, rollData).roll();
- roll.toMessage({
- speaker: ChatMessage.getSpeaker({actor: this.actor}),
- flavor: title,
- rollMode: game.settings.get("core", "rollMode"),
- messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id }}
- });
- return roll;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Perform an ability recharge test for an item which uses the d6 recharge mechanic
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- async rollRecharge() {
- const data = this.data.data;
- if ( !data.recharge.value ) return;
-
- // Roll the check
- const roll = new Roll("1d6").roll();
- const success = roll.total >= parseInt(data.recharge.value);
-
- // Display a Chat Message
- const promises = [roll.toMessage({
- flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure")}`,
- speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
- })];
-
- // Update the Item data
- if ( success ) promises.push(this.update({"data.recharge.charged": true}));
- return Promise.all(promises).then(() => roll);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation
- * @prarm {Object} options Roll configuration options provided to the d20Roll function
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollToolCheck(options={}) {
- if ( this.type !== "tool" ) throw "Wrong item type!";
-
- // Prepare roll data
- let rollData = this.getRollData();
- const parts = [`@mod`, "@prof"];
- const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
-
- // Add global actor bonus
- const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {};
- if ( bonuses.check ) {
- parts.push("@checkBonus");
- rollData.checkBonus = bonuses.check;
- }
-
- // Compose the roll data
- const rollConfig = mergeObject({
- parts: parts,
- data: rollData,
- title: title,
- speaker: ChatMessage.getSpeaker({actor: this.actor}),
- flavor: title,
- dialogOptions: {
- width: 400,
- top: options.event ? options.event.clientY - 80 : null,
- left: window.innerWidth - 710,
- },
- chooseModifier: true,
- halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false,
- reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"),
- messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }}
- }, options);
- rollConfig.event = options.event;
-
- // Call the roll helper utility
- return d20Roll(rollConfig);
- }
-
- /* -------------------------------------------- */
-
- /**
- * Prepare a data object which is passed to any Roll formulas which are created related to this Item
- * @private
- */
- getRollData() {
- if ( !this.actor ) return null;
- const rollData = this.actor.getRollData();
- rollData.item = foundry.utils.deepClone(this.data.data);
-
- // Include an ability score modifier if one exists
- const abl = this.abilityMod;
- if ( abl ) {
- const ability = rollData.abilities[abl];
- if ( !ability ) {
- console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`);
- }
- rollData["mod"] = ability?.mod || 0;
- }
-
- // Include a proficiency score
- const prof = ("proficient" in rollData.item) ? (rollData.item.proficient || 0) : 1;
- rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0));
- return rollData;
- }
-
- /* -------------------------------------------- */
- /* Chat Message Helpers */
- /* -------------------------------------------- */
-
- static chatListeners(html) {
- html.on('click', '.card-buttons button', this._onChatCardAction.bind(this));
- html.on('click', '.item-name', this._onChatCardToggleContent.bind(this));
- }
-
- /* -------------------------------------------- */
-
- /**
- * Handle execution of a chat card action via a click event on one of the card buttons
- * @param {Event} event The originating click event
- * @returns {Promise} A promise which resolves once the handler workflow is complete
- * @private
- */
- static async _onChatCardAction(event) {
- event.preventDefault();
-
- // Extract card data
- const button = event.currentTarget;
- button.disabled = true;
- const card = button.closest(".chat-card");
- const messageId = card.closest(".message").dataset.messageId;
- const message = game.messages.get(messageId);
- const action = button.dataset.action;
-
- // Validate permission to proceed with the roll
- const isTargetted = action === "save";
- if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return;
-
- // Recover the actor for the chat card
- const actor = await this._getChatCardActor(card);
- if ( !actor ) return;
-
- // Get the Item from stored flag data or by the item ID on the Actor
- const storedData = message.getFlag("sw5e", "itemData");
- const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(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;
-
- // Handle different actions
- switch ( action ) {
- case "attack":
- await item.rollAttack({event}); break;
- case "damage":
- case "versatile":
- await item.rollDamage({
- critical: event.altKey,
- event: event,
- powerLevel: powerLevel,
- versatile: action === "versatile"
+ // Determine whether the item can be used by testing for resource consumption
+ const usage = item._getUsageUpdates({
+ consumeRecharge,
+ consumeResource,
+ consumePowerLevel,
+ consumeUsage,
+ consumeQuantity
});
- 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 });
+ if (!usage) return;
+
+ const {actorUpdates, itemUpdates, resourceUpdates} = usage;
+
+ // Commit pending data updates
+ if (!foundry.utils.isObjectEmpty(itemUpdates)) await item.update(itemUpdates);
+ if (consumeQuantity && item.data.data.quantity === 0) await item.delete();
+ if (!foundry.utils.isObjectEmpty(actorUpdates)) await actor.update(actorUpdates);
+ if (!foundry.utils.isObjectEmpty(resourceUpdates)) {
+ const resource = actor.items.get(id.consume?.target);
+ if (resource) await resource.update(resourceUpdates);
}
- break;
- case "toolCheck":
- await item.rollToolCheck({event}); break;
- case "placeTemplate":
- const template = game.sw5e.canvas.AbilityTemplate.fromItem(item);
- if ( template ) template.drawPreview();
- break;
+
+ // Initiate measured template creation
+ if (createMeasuredTemplate) {
+ const template = game.sw5e.canvas.AbilityTemplate.fromItem(item);
+ if (template) template.drawPreview();
+ }
+
+ // Create or return the Chat Message data
+ return item.displayCard({rollMode, createMessage});
}
- // Re-enable the button
- button.disabled = false;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Verify that the consumed resources used by an Item are available.
+ * Otherwise display an error and return false.
+ * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
+ * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
+ * @param {boolean} consumeResource Whether the item consumes a limited resource
+ * @param {string|null} consumePowerLevel The category of power slot to consume, or null
+ * @param {boolean} consumeUsage Whether the item consumes a limited usage
+ * @returns {object|boolean} A set of data changes to apply when the item is used, or false
+ * @private
+ */
+ _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) {
+ // Reference item data
+ const id = this.data.data;
+ const actorUpdates = {};
+ const itemUpdates = {};
+ const resourceUpdates = {};
- /**
- * Handle toggling the visibility of chat card content when the name is clicked
- * @param {Event} event The originating click event
- * @private
- */
- static _onChatCardToggleContent(event) {
- event.preventDefault();
- const header = event.currentTarget;
- const card = header.closest(".chat-card");
- const content = card.querySelector(".card-content");
- content.style.display = content.style.display === "none" ? "block" : "none";
- }
+ // Consume Recharge
+ if (consumeRecharge) {
+ const recharge = id.recharge || {};
+ if (recharge.charged === false) {
+ ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
+ return false;
+ }
+ itemUpdates["data.recharge.charged"] = false;
+ }
- /* -------------------------------------------- */
+ // Consume Limited Resource
+ if (consumeResource) {
+ const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates);
+ if (canConsume === false) return false;
+ }
- /**
- * Get the Actor which is the author of a chat card
- * @param {HTMLElement} card The chat card being used
- * @return {Actor|null} The Actor entity or null
- * @private
- */
- static async _getChatCardActor(card) {
+ // Consume Power Slots and Force/Tech Points
+ if (consumePowerLevel) {
+ if (Number.isNumeric(consumePowerLevel)) consumePowerLevel = `power${consumePowerLevel}`;
+ const level = this.actor?.data.data.powers[consumePowerLevel];
+ const fp = this.actor.data.data.attributes.force.points;
+ const tp = this.actor.data.data.attributes.tech.points;
+ const powerCost = id.level + 1;
+ const innatePower = this.actor.data.data.attributes.powercasting === "innate";
+ if (!innatePower) {
+ switch (id.school) {
+ case "lgt":
+ case "uni":
+ case "drk": {
+ const powers = Number(level?.fvalue ?? 0);
+ if (powers === 0) {
+ const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
+ ui.notifications.warn(
+ game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})
+ );
+ return false;
+ }
+ actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0);
+ if (fp.temp >= powerCost) {
+ actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost;
+ } else {
+ actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost;
+ actorUpdates["data.attributes.force.points.temp"] = 0;
+ }
+ break;
+ }
+ case "tec": {
+ const powers = Number(level?.tvalue ?? 0);
+ if (powers === 0) {
+ const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
+ ui.notifications.warn(
+ game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})
+ );
+ return false;
+ }
+ actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0);
+ if (tp.temp >= powerCost) {
+ actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost;
+ } else {
+ actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost;
+ actorUpdates["data.attributes.tech.points.temp"] = 0;
+ }
+ break;
+ }
+ }
+ }
+ }
- // Case 1 - a synthetic actor from a Token
- if ( card.dataset.tokenId ) {
- const token = await fromUuid(card.dataset.tokenId);
- if ( !token ) return null;
- return token.actor;
+ // Consume Limited Usage
+ if (consumeUsage) {
+ const uses = id.uses || {};
+ const available = Number(uses.value ?? 0);
+ let used = false;
+
+ // Reduce usages
+ const remaining = Math.max(available - 1, 0);
+ if (available >= 1) {
+ used = true;
+ itemUpdates["data.uses.value"] = remaining;
+ }
+
+ // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity
+ if (consumeQuantity && (!used || remaining === 0)) {
+ const q = Number(id.quantity ?? 1);
+ if (q >= 1) {
+ used = true;
+ itemUpdates["data.quantity"] = Math.max(q - 1, 0);
+ itemUpdates["data.uses.value"] = uses.max ?? 1;
+ }
+ }
+
+ // If the item was not used, return a warning
+ if (!used) {
+ ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
+ return false;
+ }
+ }
+
+ // Return the configured usage
+ return {itemUpdates, actorUpdates, resourceUpdates};
}
- // Case 2 - use Actor ID directory
- const actorId = card.dataset.actorId;
- return game.actors.get(actorId) || null;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Handle update actions required when consuming an external resource
+ * @param {object} itemUpdates An object of data updates applied to this item
+ * @param {object} actorUpdates An object of data updates applied to the item owner (Actor)
+ * @param {object} resourceUpdates An object of data updates applied to a different resource item (Item)
+ * @return {boolean|void} Return false to block further progress, or return nothing to continue
+ * @private
+ */
+ _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) {
+ const actor = this.actor;
+ const itemData = this.data.data;
+ const consume = itemData.consume || {};
+ if (!consume.type) return;
- /**
- * Get the Actor which is the author of a chat card
- * @param {HTMLElement} card The chat card being used
- * @return {Actor[]} An Array of Actor entities, if any
- * @private
- */
- static _getChatCardTargets(card) {
- 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;
- }
+ // No consumed target
+ const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
+ if (!consume.target) {
+ ui.notifications.warn(
+ game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})
+ );
+ return false;
+ }
- /* -------------------------------------------- */
- /* Event Handlers */
- /* -------------------------------------------- */
+ // Identify the consumed resource and its current quantity
+ let resource = null;
+ let amount = Number(consume.amount ?? 1);
+ let quantity = 0;
+ switch (consume.type) {
+ case "attribute":
+ resource = getProperty(actor.data.data, consume.target);
+ quantity = resource || 0;
+ break;
+ case "ammo":
+ case "material":
+ resource = actor.items.get(consume.target);
+ quantity = resource ? resource.data.data.quantity : 0;
+ break;
+ case "charges":
+ resource = actor.items.get(consume.target);
+ if (!resource) break;
+ const uses = resource.data.data.uses;
+ if (uses.per && uses.max) quantity = uses.value;
+ else if (resource.data.data.recharge?.value) {
+ quantity = resource.data.data.recharge.charged ? 1 : 0;
+ amount = 1;
+ }
+ break;
+ }
- /** @inheritdoc */
- async _preCreate(data, options, user) {
- await super._preCreate(data, options, user);
- if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return;
- const actorData = this.parent.data;
- const isNPC = this.parent.type === "npc";
- let updates;
- switch (data.type) {
- case "equipment":
- updates = this._onCreateOwnedEquipment(data, actorData, isNPC);
- break;
- case "weapon":
- updates = this._onCreateOwnedWeapon(data, actorData, isNPC);
- break;
- case "power":
- updates = this._onCreateOwnedPower(data, actorData, isNPC);
- break;
+ // Verify that a consumed resource is available
+ if (!resource) {
+ ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
+ return false;
+ }
+
+ // Verify that the required quantity is available
+ let remaining = quantity - amount;
+ if (remaining < 0) {
+ ui.notifications.warn(
+ game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel})
+ );
+ return false;
+ }
+
+ // Define updates to provided data objects
+ switch (consume.type) {
+ case "attribute":
+ actorUpdates[`data.${consume.target}`] = remaining;
+ break;
+ case "ammo":
+ case "material":
+ resourceUpdates["data.quantity"] = remaining;
+ break;
+ case "charges":
+ const uses = resource.data.data.uses || {};
+ const recharge = resource.data.data.recharge || {};
+ if (uses.per && uses.max) resourceUpdates["data.uses.value"] = remaining;
+ else if (recharge.value) resourceUpdates["data.recharge.charged"] = false;
+ break;
+ }
}
- if (updates) return this.data.update(updates);
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @inheritdoc */
- _onCreate(data, options, userId) {
- super._onCreate(data, options, userId);
+ /**
+ * Display the chat card for an Item as a Chat Message
+ * @param {object} options Options which configure the display of the item chat card
+ * @param {string} rollMode The message visibility mode to apply to the created card
+ * @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return
+ * the prepared message data (if false)
+ */
+ async displayCard({rollMode, createMessage = true} = {}) {
+ // Render the chat card template
+ const token = this.actor.token;
+ const templateData = {
+ actor: this.actor,
+ tokenId: token?.uuid || null,
+ item: this.data,
+ data: this.getChatData(),
+ labels: this.labels,
+ hasAttack: this.hasAttack,
+ isHealing: this.isHealing,
+ hasDamage: this.hasDamage,
+ isVersatile: this.isVersatile,
+ isPower: this.data.type === "power",
+ hasSave: this.hasSave,
+ hasAreaTarget: this.hasAreaTarget,
+ isTool: this.data.type === "tool"
+ };
+ const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData);
- // The below options are only needed for character classes
- if ( userId !== game.user.id ) return;
- const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class");
- if ( !isCharacterClass ) return;
+ // Create the ChatMessage data object
+ const chatData = {
+ user: game.user.data._id,
+ type: CONST.CHAT_MESSAGE_TYPES.OTHER,
+ content: html,
+ flavor: this.data.data.chatFlavor || this.name,
+ speaker: ChatMessage.getSpeaker({actor: this.actor, token}),
+ flags: {"core.canPopout": true}
+ };
- // Assign a new primary class
- const pc = this.parent.items.get(this.parent.data.data.details.originalClass);
- if ( !pc ) this.parent._assignPrimaryClass();
+ // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
+ if (this.data.type === "consumable" && !this.actor.items.has(this.id)) {
+ chatData.flags["sw5e.itemData"] = this.data;
+ }
- // Prompt to add new class features
- if (options.addFeatures === false) return;
- this.parent.getClassFeatures({
- className: this.name,
- archetypeName: this.data.data.archetype,
- level: this.data.data.levels
- }).then(features => {
- return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
- });
- }
+ // Apply the roll mode to adjust message visibility
+ ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode"));
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- _onUpdate(changed, options, userId) {
- super._onUpdate(changed, options, userId);
-
- // The below options are only needed for character classes
- if ( userId !== game.user.id ) return;
- const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class");
- if ( !isCharacterClass ) return;
-
- // Prompt to add new class features
- const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some(k => k in changed.data));
- if ( !addFeatures || (options.addFeatures === false) ) return;
- this.parent.getClassFeatures({
- className: changed.name || this.name,
- archetypeName: changed.data?.archetype || this.data.data.archetype,
- level: changed.data?.levels || this.data.data.levels
- }).then(features => {
- return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
- });
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- _onDelete(options, userId) {
- super._onDelete(options, userId);
-
- // Assign a new primary class
- if ( this.parent && (this.type === "class") && (userId === game.user.id) ) {
- if ( this.id !== this.parent.data.data.details.originalClass ) return;
- this.parent._assignPrimaryClass();
+ // Create the Chat Message or return its data
+ return createMessage ? ChatMessage.create(chatData) : chatData;
}
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
+ /* Chat Cards */
+ /* -------------------------------------------- */
- /**
- * Pre-creation logic for the automatic configuration of owned equipment type Items
- * @private
- */
- _onCreateOwnedEquipment(data, actorData, isNPC) {
- const updates = {};
- if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) {
- updates["data.equipped"] = isNPC; // NPCs automatically equip equipment
+ /**
+ * Prepare an object of chat data used to display a card for the Item in the chat log
+ * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function
+ * @return {Object} An object of chat data to render
+ */
+ getChatData(htmlOptions = {}) {
+ const data = foundry.utils.deepClone(this.data.data);
+ const labels = this.labels;
+
+ // Rich text description
+ data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions);
+
+ // Item type specific properties
+ const props = [];
+ const fn = this[`_${this.data.type}ChatData`];
+ if (fn) fn.bind(this)(data, labels, props);
+
+ // Equipment properties
+ if (data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type)) {
+ if (data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED)
+ props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED]));
+ props.push(
+ game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
+ game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient")
+ );
+ }
+
+ // Ability activation properties
+ if (data.hasOwnProperty("activation")) {
+ props.push(
+ labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""),
+ labels.target,
+ labels.range,
+ labels.duration
+ );
+ }
+
+ // Filter properties and return
+ data.properties = props.filter((p) => !!p);
+ return data;
}
- if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
- if ( isNPC ) {
- updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
- } else {
- const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency
- const actorArmorProfs = actorData.data.traits?.armorProf?.value || [];
- updates["data.proficient"] = (armorProf === true) || actorArmorProfs.includes(armorProf);
- }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare chat card data for equipment type items
+ * @private
+ */
+ _equipmentChatData(data, labels, props) {
+ props.push(
+ CONFIG.SW5E.equipmentTypes[data.armor.type],
+ labels.armor || null,
+ data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null
+ );
}
- return updates;
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Pre-creation logic for the automatic configuration of owned power type Items
- * @private
- */
- _onCreateOwnedPower(data, actorData, isNPC) {
- const updates = {};
- updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone
- return updates;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Pre-creation logic for the automatic configuration of owned weapon type Items
- * @private
- */
- _onCreateOwnedWeapon(data, actorData, isNPC) {
- const updates = {};
- if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) {
- updates["data.equipped"] = isNPC; // NPCs automatically equip weapons
+ /**
+ * Prepare chat card data for weapon type items
+ * @private
+ */
+ _weaponChatData(data, labels, props) {
+ props.push(CONFIG.SW5E.weaponTypes[data.weaponType]);
}
- if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
- if ( isNPC ) {
- updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
- } else {
- // TODO: With the changes to make weapon proficiencies more verbose, this may need revising
- const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency
- const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || [];
- updates["data.proficient"] = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
- }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare chat card data for consumable type items
+ * @private
+ */
+ _consumableChatData(data, labels, props) {
+ props.push(
+ CONFIG.SW5E.consumableTypes[data.consumableType],
+ data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges")
+ );
+ data.hasCharges = data.uses.value >= 0;
}
- return updates;
- }
- /* -------------------------------------------- */
- /* Factory Methods */
- /* -------------------------------------------- */
-// TODO: Make work properly
- /**
- * Create a consumable power scroll Item from a power Item.
- * @param {Item5e} power The power to be made into a scroll
- * @return {Item5e} The created scroll consumable item
- */
- static async createScrollFromPower(power) {
+ /* -------------------------------------------- */
- // Get power data
- const itemData = power instanceof Item5e ? power.data : power;
- const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data;
+ /**
+ * Prepare chat card data for tool type items
+ * @private
+ */
+ _toolChatData(data, labels, props) {
+ props.push(CONFIG.SW5E.abilities[data.ability] || null, CONFIG.SW5E.proficiencyLevels[data.proficient || 0]);
+ }
- // Get scroll data
- const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`;
- const scrollItem = await fromUuid(scrollUuid);
- const scrollData = scrollItem.data;
- delete scrollData._id;
+ /* -------------------------------------------- */
- // Split the scroll description into an intro paragraph and the remaining details
- const scrollDescription = scrollData.data.description.value;
- const pdel = '
';
- const scrollIntroEnd = scrollDescription.indexOf(pdel);
- const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);
- const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);
+ /**
+ * Prepare chat card data for loot type items
+ * @private
+ */
+ _lootChatData(data, labels, props) {
+ props.push(
+ game.i18n.localize("SW5E.ItemTypeLoot"),
+ data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null
+ );
+ }
- // Create a composite description from the scroll description and the power details
- const desc = `${scrollIntro}${itemData.name} (Level ${level}) ${description.value}Scroll Details ${scrollDetails}`;
+ /* -------------------------------------------- */
- // Create the power scroll data
- const powerScrollData = mergeObject(scrollData, {
- name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`,
- img: itemData.img,
- data: {
- "description.value": desc.trim(),
- source,
- actionType,
- activation,
- duration,
- target,
- range,
- damage,
- save,
- level
- }
- });
- return new this(powerScrollData);
- }
+ /**
+ * Render a chat card for Power type data
+ * @return {Object}
+ * @private
+ */
+ _powerChatData(data, labels, props) {
+ props.push(labels.level, labels.components + (labels.materials ? ` (${labels.materials})` : ""));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare chat card data for items of the "Feat" type
+ * @private
+ */
+ _featChatData(data, labels, props) {
+ props.push(data.requirements);
+ }
+
+ /* -------------------------------------------- */
+ /* Item Rolls - Attack, Damage, Saves, Checks */
+ /* -------------------------------------------- */
+
+ /**
+ * Place an attack roll using an item (weapon, feat, power, or equipment)
+ * Rely upon the d20Roll logic for the core implementation
+ *
+ * @param {object} options Roll options which are configured and provided to the d20Roll function
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ async rollAttack(options = {}) {
+ const itemData = this.data.data;
+ const flags = this.actor.data.flags.sw5e || {};
+ if (!this.hasAttack) {
+ throw new Error("You may not place an Attack Roll with this Item.");
+ }
+ let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`;
+
+ // get the parts and rollData for this item's attack
+ const {parts, rollData} = this.getAttackToHit();
+
+ // Handle ammunition consumption
+ delete this._ammo;
+ let ammo = null;
+ let ammoUpdate = null;
+ const consume = itemData.consume;
+ if (consume?.type === "ammo") {
+ 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;
+ title += ` [${ammo.name}]`;
+ }
+ }
+
+ // Get pending ammunition update
+ const usage = this._getUsageUpdates({consumeResource: true});
+ if (usage === false) return null;
+ ammoUpdate = usage.resourceUpdates || {};
+ }
+
+ // 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,
+ top: options.event ? options.event.clientY - 80 : null,
+ left: window.innerWidth - 710
+ },
+ messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id}}
+ },
+ options
+ );
+ rollConfig.event = options.event;
+
+ // 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
+ if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) {
+ rollConfig.elvenAccuracy = true;
+ }
+
+ // Apply Halfling Lucky
+ if (flags.halflingLucky) rollConfig.halflingLucky = true;
+
+ // Invoke the d20 roll helper
+ const roll = await d20Roll(rollConfig);
+ if (roll === false) return null;
+
+ // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
+ if (ammo && !isObjectEmpty(ammoUpdate)) await ammo.update(ammoUpdate);
+ return roll;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Place a damage roll using an item (weapon, feat, power, or equipment)
+ * Rely upon the damageRoll logic for the core implementation.
+ * @param {MouseEvent} [event] An event which triggered this roll, if any
+ * @param {boolean} [critical] Should damage be rolled as a critical hit?
+ * @param {number} [powerLevel] If the item is a power, override the level for damage scaling
+ * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula
+ * @param {object} [options] Additional options passed to the damageRoll function
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollDamage({critical = false, event = null, powerLevel = null, versatile = false, options = {}} = {}) {
+ if (!this.hasDamage) throw new Error("You may not make a Damage Roll with this Item.");
+ const itemData = this.data.data;
+ const actorData = this.actor.data.data;
+ 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;
+
+ // Configure the damage roll
+ const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll");
+ const title = `${this.name} - ${actionFlavor}`;
+ const rollConfig = {
+ actor: this.actor,
+ critical: critical ?? event?.altKey ?? false,
+ data: rollData,
+ event: event,
+ fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false,
+ parts: parts,
+ title: title,
+ flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
+ speaker: ChatMessage.getSpeaker({actor: this.actor}),
+ 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) {
+ parts[0] = itemData.damage.versatile;
+ messageData["flags.sw5e.roll"].versatile = true;
+ }
+
+ // Scale damage from up-casting powers
+ if (this.data.type === "power") {
+ if (itemData.scaling.mode === "atwill") {
+ const level =
+ this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
+ this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData);
+ } else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) {
+ const scaling = itemData.scaling.formula;
+ this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData);
+ }
+ }
+
+ // Add damage bonus formula
+ const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {};
+ if (actorBonus.damage && parseInt(actorBonus.damage) !== 0) {
+ parts.push(actorBonus.damage);
+ }
+
+ // Handle ammunition damage
+ const ammoData = this._ammo?.data;
+
+ // only add the ammunition damage if the ammution is a consumable with type 'ammo'
+ if (this._ammo && ammoData.type === "consumable" && ammoData.data.consumableType === "ammo") {
+ parts.push("@ammo");
+ rollData["ammo"] = ammoData.data.damage.parts.map((p) => p[0]).join("+");
+ 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(mergeObject(rollConfig, options));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Adjust an at-will damage formula to scale it for higher level characters and monsters
+ * @private
+ */
+ _scaleAtWillDamage(parts, scale, level, rollData) {
+ const add = Math.floor((level + 1) / 6);
+ if (add === 0) return;
+ this._scaleDamage(parts, scale || parts.join(" + "), add, rollData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Adjust the power damage formula to scale it for power level up-casting
+ * @param {Array} parts The original damage parts
+ * @param {number} baseLevel The default power level
+ * @param {number} powerLevel The casted power level
+ * @param {string} formula The scaling formula
+ * @param {object} rollData A data object that should be applied to the scaled damage roll
+ * @return {string[]} The scaled roll parts
+ * @private
+ */
+ _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) {
+ const upcastLevels = Math.max(powerLevel - baseLevel, 0);
+ if (upcastLevels === 0) return parts;
+ this._scaleDamage(parts, formula, upcastLevels, rollData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Scale an array of damage parts according to a provided scaling formula and scaling multiplier
+ * @param {string[]} parts Initial roll parts
+ * @param {string} scaling A scaling formula
+ * @param {number} times A number of times to apply the scaling formula
+ * @param {object} rollData A data object that should be applied to the scaled damage roll
+ * @return {string[]} The scaled roll parts
+ * @private
+ */
+ _scaleDamage(parts, scaling, times, rollData) {
+ if (times <= 0) return parts;
+ const p0 = new Roll(parts[0], rollData);
+ const s = new Roll(scaling, rollData).alter(times);
+
+ // Attempt to simplify by combining like dice terms
+ let simplified = false;
+ if (s.terms[0] instanceof Die && s.terms.length === 1) {
+ const d0 = p0.terms[0];
+ const s0 = s.terms[0];
+ if (d0 instanceof Die && d0.faces === s0.faces && d0.modifiers.equals(s0.modifiers)) {
+ d0.number += s0.number;
+ parts[0] = p0.formula;
+ simplified = true;
+ }
+ }
+
+ // Otherwise add to the first part
+ if (!simplified) {
+ parts[0] = `${parts[0]} + ${s.formula}`;
+ }
+ return parts;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Place an attack roll using an item (weapon, feat, power, or equipment)
+ * Rely upon the d20Roll logic for the core implementation
+ *
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ async rollFormula(options = {}) {
+ if (!this.data.data.formula) {
+ throw new Error("This Item does not have a formula to roll!");
+ }
+
+ // Define Roll Data
+ const rollData = this.getRollData();
+ if (options.powerLevel) rollData.item.level = options.powerLevel;
+ const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`;
+
+ // Invoke the roll and submit it to chat
+ const roll = new Roll(rollData.item.formula, rollData).roll();
+ roll.toMessage({
+ speaker: ChatMessage.getSpeaker({actor: this.actor}),
+ flavor: title,
+ rollMode: game.settings.get("core", "rollMode"),
+ messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id}}
+ });
+ return roll;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Perform an ability recharge test for an item which uses the d6 recharge mechanic
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ async rollRecharge() {
+ const data = this.data.data;
+ if (!data.recharge.value) return;
+
+ // Roll the check
+ const roll = new Roll("1d6").roll();
+ const success = roll.total >= parseInt(data.recharge.value);
+
+ // Display a Chat Message
+ const promises = [
+ roll.toMessage({
+ flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(
+ success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure"
+ )}`,
+ speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
+ })
+ ];
+
+ // Update the Item data
+ if (success) promises.push(this.update({"data.recharge.charged": true}));
+ return Promise.all(promises).then(() => roll);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation
+ * @prarm {Object} options Roll configuration options provided to the d20Roll function
+ * @return {Promise} A Promise which resolves to the created Roll instance
+ */
+ rollToolCheck(options = {}) {
+ if (this.type !== "tool") throw "Wrong item type!";
+
+ // Prepare roll data
+ let rollData = this.getRollData();
+ const parts = [`@mod`, "@prof"];
+ const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
+
+ // Add global actor bonus
+ const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {};
+ if (bonuses.check) {
+ parts.push("@checkBonus");
+ rollData.checkBonus = bonuses.check;
+ }
+
+ // Compose the roll data
+ const rollConfig = mergeObject(
+ {
+ parts: parts,
+ data: rollData,
+ title: title,
+ speaker: ChatMessage.getSpeaker({actor: this.actor}),
+ flavor: title,
+ dialogOptions: {
+ width: 400,
+ top: options.event ? options.event.clientY - 80 : null,
+ left: window.innerWidth - 710
+ },
+ chooseModifier: true,
+ halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false,
+ reliableTalent: this.data.data.proficient >= 1 && this.actor.getFlag("sw5e", "reliableTalent"),
+ messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}}
+ },
+ options
+ );
+ rollConfig.event = options.event;
+
+ // Call the roll helper utility
+ return d20Roll(rollConfig);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare a data object which is passed to any Roll formulas which are created related to this Item
+ * @private
+ */
+ getRollData() {
+ if (!this.actor) return null;
+ const rollData = this.actor.getRollData();
+ rollData.item = foundry.utils.deepClone(this.data.data);
+
+ // Include an ability score modifier if one exists
+ const abl = this.abilityMod;
+ if (abl) {
+ const ability = rollData.abilities[abl];
+ if (!ability) {
+ console.warn(
+ `Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`
+ );
+ }
+ rollData["mod"] = ability?.mod || 0;
+ }
+
+ // Include a proficiency score
+ const prof = "proficient" in rollData.item ? rollData.item.proficient || 0 : 1;
+ rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0));
+ return rollData;
+ }
+
+ /* -------------------------------------------- */
+ /* Chat Message Helpers */
+ /* -------------------------------------------- */
+
+ static chatListeners(html) {
+ html.on("click", ".card-buttons button", this._onChatCardAction.bind(this));
+ html.on("click", ".item-name", this._onChatCardToggleContent.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle execution of a chat card action via a click event on one of the card buttons
+ * @param {Event} event The originating click event
+ * @returns {Promise} A promise which resolves once the handler workflow is complete
+ * @private
+ */
+ static async _onChatCardAction(event) {
+ event.preventDefault();
+
+ // Extract card data
+ const button = event.currentTarget;
+ button.disabled = true;
+ const card = button.closest(".chat-card");
+ const messageId = card.closest(".message").dataset.messageId;
+ const message = game.messages.get(messageId);
+ const action = button.dataset.action;
+
+ // Validate permission to proceed with the roll
+ const isTargetted = action === "save";
+ if (!(isTargetted || game.user.isGM || message.isAuthor)) return;
+
+ // Recover the actor for the chat card
+ const actor = await this._getChatCardActor(card);
+ if (!actor) return;
+
+ // Get the Item from stored flag data or by the item ID on the Actor
+ const storedData = message.getFlag("sw5e", "itemData");
+ const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(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;
+
+ // Handle different actions
+ switch (action) {
+ case "attack":
+ await item.rollAttack({event});
+ break;
+ case "damage":
+ case "versatile":
+ await item.rollDamage({
+ critical: event.altKey,
+ event: event,
+ powerLevel: powerLevel,
+ versatile: action === "versatile"
+ });
+ 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 = game.sw5e.canvas.AbilityTemplate.fromItem(item);
+ if (template) template.drawPreview();
+ break;
+ }
+
+ // Re-enable the button
+ button.disabled = false;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling the visibility of chat card content when the name is clicked
+ * @param {Event} event The originating click event
+ * @private
+ */
+ static _onChatCardToggleContent(event) {
+ event.preventDefault();
+ const header = event.currentTarget;
+ const card = header.closest(".chat-card");
+ const content = card.querySelector(".card-content");
+ content.style.display = content.style.display === "none" ? "block" : "none";
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Get the Actor which is the author of a chat card
+ * @param {HTMLElement} card The chat card being used
+ * @return {Actor|null} The Actor entity or null
+ * @private
+ */
+ static async _getChatCardActor(card) {
+ // Case 1 - a synthetic actor from a Token
+ if (card.dataset.tokenId) {
+ const token = await fromUuid(card.dataset.tokenId);
+ if (!token) return null;
+ return token.actor;
+ }
+
+ // Case 2 - use Actor ID directory
+ const actorId = card.dataset.actorId;
+ return game.actors.get(actorId) || null;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Get the Actor which is the author of a chat card
+ * @param {HTMLElement} card The chat card being used
+ * @return {Actor[]} An Array of Actor entities, if any
+ * @private
+ */
+ static _getChatCardTargets(card) {
+ 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;
+ }
+
+ /* -------------------------------------------- */
+ /* Event Handlers */
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ async _preCreate(data, options, user) {
+ await super._preCreate(data, options, user);
+ if (!this.isEmbedded || this.parent.type === "vehicle") return;
+ const actorData = this.parent.data;
+ const isNPC = this.parent.type === "npc";
+ let updates;
+ switch (data.type) {
+ case "equipment":
+ updates = this._onCreateOwnedEquipment(data, actorData, isNPC);
+ break;
+ case "weapon":
+ updates = this._onCreateOwnedWeapon(data, actorData, isNPC);
+ break;
+ case "power":
+ updates = this._onCreateOwnedPower(data, actorData, isNPC);
+ break;
+ }
+ if (updates) return this.data.update(updates);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ _onCreate(data, options, userId) {
+ super._onCreate(data, options, userId);
+
+ // The below options are only needed for character classes
+ if (userId !== game.user.id) return;
+ const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class";
+ if (!isCharacterClass) return;
+
+ // Assign a new primary class
+ const pc = this.parent.items.get(this.parent.data.data.details.originalClass);
+ if (!pc) this.parent._assignPrimaryClass();
+
+ // Prompt to add new class features
+ if (options.addFeatures === false) return;
+ this.parent
+ .getClassFeatures({
+ className: this.name,
+ archetypeName: this.data.data.archetype,
+ level: this.data.data.levels
+ })
+ .then((features) => {
+ return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ _onUpdate(changed, options, userId) {
+ super._onUpdate(changed, options, userId);
+
+ // The below options are only needed for character classes
+ if (userId !== game.user.id) return;
+ const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class";
+ if (!isCharacterClass) return;
+
+ // Prompt to add new class features
+ const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some((k) => k in changed.data));
+ if (!addFeatures || options.addFeatures === false) return;
+ this.parent
+ .getClassFeatures({
+ className: changed.name || this.name,
+ archetypeName: changed.data?.archetype || this.data.data.archetype,
+ level: changed.data?.levels || this.data.data.levels
+ })
+ .then((features) => {
+ return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ _onDelete(options, userId) {
+ super._onDelete(options, userId);
+
+ // Assign a new primary class
+ if (this.parent && this.type === "class" && userId === game.user.id) {
+ if (this.id !== this.parent.data.data.details.originalClass) return;
+ this.parent._assignPrimaryClass();
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Pre-creation logic for the automatic configuration of owned equipment type Items
+ * @private
+ */
+ _onCreateOwnedEquipment(data, actorData, isNPC) {
+ const updates = {};
+ if (foundry.utils.getProperty(data, "data.equipped") === undefined) {
+ updates["data.equipped"] = isNPC; // NPCs automatically equip equipment
+ }
+ if (foundry.utils.getProperty(data, "data.proficient") === undefined) {
+ if (isNPC) {
+ updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
+ } else {
+ const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency
+ const actorArmorProfs = actorData.data.traits?.armorProf?.value || [];
+ updates["data.proficient"] = armorProf === true || actorArmorProfs.includes(armorProf);
+ }
+ }
+ return updates;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Pre-creation logic for the automatic configuration of owned power type Items
+ * @private
+ */
+ _onCreateOwnedPower(data, actorData, isNPC) {
+ const updates = {};
+ updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone
+ return updates;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Pre-creation logic for the automatic configuration of owned weapon type Items
+ * @private
+ */
+ _onCreateOwnedWeapon(data, actorData, isNPC) {
+ const updates = {};
+ if (foundry.utils.getProperty(data, "data.equipped") === undefined) {
+ updates["data.equipped"] = isNPC; // NPCs automatically equip weapons
+ }
+ if (foundry.utils.getProperty(data, "data.proficient") === undefined) {
+ if (isNPC) {
+ updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
+ } else {
+ // TODO: With the changes to make weapon proficiencies more verbose, this may need revising
+ const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency
+ const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || [];
+ updates["data.proficient"] = weaponProf === true || actorWeaponProfs.includes(weaponProf);
+ }
+ }
+ return updates;
+ }
+
+ /* -------------------------------------------- */
+ /* Factory Methods */
+ /* -------------------------------------------- */
+ // TODO: Make work properly
+ /**
+ * Create a consumable power scroll Item from a power Item.
+ * @param {Item5e} power The power to be made into a scroll
+ * @return {Item5e} The created scroll consumable item
+ */
+ static async createScrollFromPower(power) {
+ // Get power data
+ const itemData = power instanceof Item5e ? power.data : power;
+ const {actionType, description, source, activation, duration, target, range, damage, save, level} =
+ itemData.data;
+
+ // Get scroll data
+ const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`;
+ const scrollItem = await fromUuid(scrollUuid);
+ const scrollData = scrollItem.data;
+ delete scrollData._id;
+
+ // Split the scroll description into an intro paragraph and the remaining details
+ const scrollDescription = scrollData.data.description.value;
+ const pdel = "";
+ const scrollIntroEnd = scrollDescription.indexOf(pdel);
+ const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);
+ const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);
+
+ // Create a composite description from the scroll description and the power details
+ const desc = `${scrollIntro}${itemData.name} (Level ${level}) ${description.value}Scroll Details ${scrollDetails}`;
+
+ // Create the power scroll data
+ const powerScrollData = mergeObject(scrollData, {
+ name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`,
+ img: itemData.img,
+ data: {
+ "description.value": desc.trim(),
+ source,
+ actionType,
+ activation,
+ duration,
+ target,
+ range,
+ damage,
+ save,
+ level
+ }
+ });
+ return new this(powerScrollData);
+ }
}
diff --git a/module/item/sheet.js b/module/item/sheet.js
index ba3cfe06..0bb7f64b 100644
--- a/module/item/sheet.js
+++ b/module/item/sheet.js
@@ -1,361 +1,370 @@
import TraitSelector from "../apps/trait-selector.js";
-import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js";
+import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
/**
* Override and extend the core ItemSheet implementation to handle specific item types
* @extends {ItemSheet}
*/
export default class ItemSheet5e extends ItemSheet {
- constructor(...args) {
- super(...args);
+ constructor(...args) {
+ super(...args);
- // Expand the default size of the class sheet
- if (this.object.data.type === "class") {
- this.options.width = this.position.width = 600;
- this.options.height = this.position.height = 680;
- }
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- width: 560,
- height: 400,
- classes: ["sw5e", "sheet", "item"],
- resizable: true,
- scrollY: [".tab.details"],
- tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }]
- });
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- get template() {
- const path = "systems/sw5e/templates/items/";
- return `${path}/${this.item.data.type}.html`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- async getData(options) {
- const data = super.getData(options);
- const itemData = data.data;
- data.labels = this.item.labels;
- data.config = CONFIG.SW5E;
-
- // Item Type, Status, and Details
- data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
- data.itemStatus = this._getItemStatus(itemData);
- data.itemProperties = this._getItemProperties(itemData);
- data.isPhysical = itemData.data.hasOwnProperty("quantity");
-
- // Potential consumption targets
- data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
-
- // Action Details
- data.hasAttackRoll = this.item.hasAttack;
- data.isHealing = itemData.data.actionType === "heal";
- data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
- data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
-
- // Original maximum uses formula
- const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
- if ( sourceMax ) itemData.data.uses.max = sourceMax;
-
- // Vehicles
- data.isCrewed = itemData.data.activation?.type === "crew";
- data.isMountable = this._isItemMountable(itemData);
-
- // Prepare Active Effects
- data.effects = prepareActiveEffectCategories(this.item.effects);
-
- // Re-define the template data references (backwards compatible)
- data.item = itemData;
- data.data = itemData.data;
- return data;
- }
-
- /* -------------------------------------------- */
-
- /**
- * Get the valid item consumption targets which exist on the actor
- * @param {Object} item Item data for the item being displayed
- * @return {{string: string}} An object of potential consumption targets
- * @private
- */
- _getItemConsumptionTargets(item) {
- const consume = item.data.consume || {};
- if (!consume.type) return [];
- const actor = this.item.actor;
- if (!actor) return {};
-
- // Ammunition
- if (consume.type === "ammo") {
- return actor.itemTypes.consumable.reduce(
- (ammo, i) => {
- if (i.data.data.consumableType === "ammo") {
- ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
- }
- return ammo;
- },
- { [item._id]: `${item.name} (${item.data.quantity})` }
- );
- }
-
- // Attributes
- else if (consume.type === "attribute") {
- const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
- attributes.bar.forEach(a => a.push("value"));
- return attributes.bar.concat(attributes.value).reduce((obj, a) => {
- let k = a.join(".");
- obj[k] = k;
- return obj;
- }, {});
- }
-
- // Materials
- else if (consume.type === "material") {
- return actor.items.reduce((obj, i) => {
- if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
- obj[i.id] = `${i.name} (${i.data.data.quantity})`;
+ // Expand the default size of the class sheet
+ if (this.object.data.type === "class") {
+ this.options.width = this.position.width = 600;
+ this.options.height = this.position.height = 680;
}
- return obj;
- }, {});
}
- // 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"
- ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})`
- : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`;
- obj[i.id] = i.name + label;
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ width: 560,
+ height: 400,
+ classes: ["sw5e", "sheet", "item"],
+ resizable: true,
+ scrollY: [".tab.details"],
+ tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ get template() {
+ const path = "systems/sw5e/templates/items/";
+ return `${path}/${this.item.data.type}.html`;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async getData(options) {
+ const data = super.getData(options);
+ const itemData = data.data;
+ data.labels = this.item.labels;
+ data.config = CONFIG.SW5E;
+
+ // Item Type, Status, and Details
+ data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
+ data.itemStatus = this._getItemStatus(itemData);
+ data.itemProperties = this._getItemProperties(itemData);
+ data.isPhysical = itemData.data.hasOwnProperty("quantity");
+
+ // Potential consumption targets
+ data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
+
+ // Action Details
+ data.hasAttackRoll = this.item.hasAttack;
+ data.isHealing = itemData.data.actionType === "heal";
+ data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
+ data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
+
+ // Original maximum uses formula
+ const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
+ if (sourceMax) itemData.data.uses.max = sourceMax;
+
+ // Vehicles
+ data.isCrewed = itemData.data.activation?.type === "crew";
+ data.isMountable = this._isItemMountable(itemData);
+
+ // Prepare Active Effects
+ data.effects = prepareActiveEffectCategories(this.item.effects);
+
+ // Re-define the template data references (backwards compatible)
+ data.item = itemData;
+ data.data = itemData.data;
+ return data;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Get the valid item consumption targets which exist on the actor
+ * @param {Object} item Item data for the item being displayed
+ * @return {{string: string}} An object of potential consumption targets
+ * @private
+ */
+ _getItemConsumptionTargets(item) {
+ const consume = item.data.consume || {};
+ if (!consume.type) return [];
+ const actor = this.item.actor;
+ if (!actor) return {};
+
+ // Ammunition
+ if (consume.type === "ammo") {
+ return actor.itemTypes.consumable.reduce(
+ (ammo, i) => {
+ if (i.data.data.consumableType === "ammo") {
+ ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
+ }
+ return ammo;
+ },
+ {[item._id]: `${item.name} (${item.data.quantity})`}
+ );
}
- // Recharging items
- const recharge = i.data.data.recharge || {};
- if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
- return obj;
- }, {});
- } else return {};
- }
+ // Attributes
+ else if (consume.type === "attribute") {
+ const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
+ attributes.bar.forEach((a) => a.push("value"));
+ return attributes.bar.concat(attributes.value).reduce((obj, a) => {
+ let k = a.join(".");
+ obj[k] = k;
+ return obj;
+ }, {});
+ }
- /* -------------------------------------------- */
+ // Materials
+ else if (consume.type === "material") {
+ return actor.items.reduce((obj, i) => {
+ if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
+ obj[i.id] = `${i.name} (${i.data.data.quantity})`;
+ }
+ return obj;
+ }, {});
+ }
- /**
- * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet
- * @return {string}
- * @private
- */
- _getItemStatus(item) {
- if (item.type === "power") {
- return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
- } else if (["weapon", "equipment"].includes(item.type)) {
- return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
- } else if (item.type === "tool") {
- return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
- }
- }
+ // 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"
+ ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
+ : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
+ max: uses.max,
+ per: uses.per
+ })})`;
+ obj[i.id] = i.name + label;
+ }
- /* -------------------------------------------- */
-
- /**
- * Get the Array of item properties which are used in the small sidebar of the description tab
- * @return {Array}
- * @private
- */
- _getItemProperties(item) {
- const props = [];
- const labels = this.item.labels;
-
- if (item.type === "weapon") {
- props.push(
- ...Object.entries(item.data.properties)
- .filter((e) => e[1] === true)
- .map((e) => CONFIG.SW5E.weaponProperties[e[0]])
- );
- } else if (item.type === "power") {
- props.push(
- labels.materials,
- item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
- item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
- );
- } else if (item.type === "equipment") {
- props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
- props.push(labels.armor);
- } else if (item.type === "feat") {
- props.push(labels.featType);
- //TODO: Work out these
- } 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 === "deployment") {
- //props.push(labels.deployment);
- } else if (item.type === "venture") {
- //props.push(labels.venture);
- } 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);
+ // Recharging items
+ const recharge = i.data.data.recharge || {};
+ if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
+ return obj;
+ }, {});
+ } else return {};
}
- // Action type
- if (item.data.actionType) {
- props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
+ /* -------------------------------------------- */
+
+ /**
+ * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet
+ * @return {string}
+ * @private
+ */
+ _getItemStatus(item) {
+ if (item.type === "power") {
+ return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
+ } else if (["weapon", "equipment"].includes(item.type)) {
+ return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
+ } else if (item.type === "tool") {
+ return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
+ }
}
- // Action usage
- if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
- props.push(labels.activation, labels.range, labels.target, labels.duration);
- }
- return props.filter((p) => !!p);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Get the Array of item properties which are used in the small sidebar of the description tab
+ * @return {Array}
+ * @private
+ */
+ _getItemProperties(item) {
+ const props = [];
+ const labels = this.item.labels;
- /**
- * Is this item a separate large object like a siege engine or vehicle
- * component that is usually mounted on fixtures rather than equipped, and
- * has its own AC and HP.
- * @param item
- * @returns {boolean}
- * @private
- */
- _isItemMountable(item) {
- const data = item.data;
- return (
- (item.type === "weapon" && data.weaponType === "siege") ||
- (item.type === "equipment" && data.armor.type === "vehicle")
- );
- }
+ if (item.type === "weapon") {
+ props.push(
+ ...Object.entries(item.data.properties)
+ .filter((e) => e[1] === true)
+ .map((e) => CONFIG.SW5E.weaponProperties[e[0]])
+ );
+ } else if (item.type === "power") {
+ props.push(
+ labels.materials,
+ item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
+ item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
+ );
+ } else if (item.type === "equipment") {
+ props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
+ props.push(labels.armor);
+ } else if (item.type === "feat") {
+ props.push(labels.featType);
+ //TODO: Work out these
+ } 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 === "deployment") {
+ //props.push(labels.deployment);
+ } else if (item.type === "venture") {
+ //props.push(labels.venture);
+ } 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);
+ }
- /* -------------------------------------------- */
+ // Action type
+ if (item.data.actionType) {
+ props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
+ }
- /** @inheritdoc */
- setPosition(position = {}) {
- if (!(this._minimized || position.height)) {
- position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
- }
- return super.setPosition(position);
- }
-
- /* -------------------------------------------- */
- /* Form Submission */
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- _getSubmitData(updateData = {}) {
- // 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 = data.data?.damage;
- if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
-
- // Return the flattened submission data
- return flattenObject(data);
- }
-
- /* -------------------------------------------- */
-
- /** @inheritdoc */
- activateListeners(html) {
- super.activateListeners(html);
- if (this.isEditable) {
- html.find(".damage-control").click(this._onDamageControl.bind(this));
- html.find(".trait-selector.class-skills").click(this._onConfigureTraits.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);
- });
- }
- }
-
- /* -------------------------------------------- */
-
- /**
- * Add or remove a damage part from the damage formula
- * @param {Event} event The original click event
- * @return {Promise}
- * @private
- */
- async _onDamageControl(event) {
- event.preventDefault();
- const a = event.currentTarget;
-
- // Add new damage component
- if (a.classList.contains("add-damage")) {
- await this._onSubmit(event); // Submit any unsaved changes
- const damage = this.item.data.data.damage;
- return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) });
+ // Action usage
+ if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
+ props.push(labels.activation, labels.range, labels.target, labels.duration);
+ }
+ return props.filter((p) => !!p);
}
- // Remove a damage component
- if (a.classList.contains("delete-damage")) {
- await this._onSubmit(event); // Submit any unsaved changes
- const li = a.closest(".damage-part");
- const damage = foundry.utils.deepClone(this.item.data.data.damage);
- damage.parts.splice(Number(li.dataset.damagePart), 1);
- return this.item.update({ "data.damage.parts": damage.parts });
+ /* -------------------------------------------- */
+
+ /**
+ * Is this item a separate large object like a siege engine or vehicle
+ * component that is usually mounted on fixtures rather than equipped, and
+ * has its own AC and HP.
+ * @param item
+ * @returns {boolean}
+ * @private
+ */
+ _isItemMountable(item) {
+ const data = item.data;
+ return (
+ (item.type === "weapon" && data.weaponType === "siege") ||
+ (item.type === "equipment" && data.armor.type === "vehicle")
+ );
}
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /**
- * Handle spawning the TraitSelector application for selection various options.
- * @param {Event} event The click event which originated the selection
- * @private
- */
- _onConfigureTraits(event) {
- event.preventDefault();
- const a = event.currentTarget;
-
- const options = {
- name: a.dataset.target,
- title: a.parentElement.innerText,
- choices: [],
- allowCustom: false
- };
-
- switch(a.dataset.options) {
- case 'saves':
- options.choices = CONFIG.SW5E.abilities;
- options.valueKey = null;
- break;
- case 'skills':
- const skills = this.item.data.data.skills;
- const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
- options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0])));
- options.maximum = skills.number;
- break;
+ /** @inheritdoc */
+ setPosition(position = {}) {
+ if (!(this._minimized || position.height)) {
+ position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
+ }
+ return super.setPosition(position);
}
- new TraitSelector(this.item, options).render(true);
- }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
+ /* Form Submission */
+ /* -------------------------------------------- */
- /** @inheritdoc */
- async _onSubmit(...args) {
- if (this._tabs[0].active === "details") this.position.height = "auto";
- await super._onSubmit(...args);
- }
+ /** @inheritdoc */
+ _getSubmitData(updateData = {}) {
+ // 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 = data.data?.damage;
+ if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
+
+ // Return the flattened submission data
+ return flattenObject(data);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ activateListeners(html) {
+ super.activateListeners(html);
+ if (this.isEditable) {
+ html.find(".damage-control").click(this._onDamageControl.bind(this));
+ html.find(".trait-selector.class-skills").click(this._onConfigureTraits.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);
+ });
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Add or remove a damage part from the damage formula
+ * @param {Event} event The original click event
+ * @return {Promise}
+ * @private
+ */
+ async _onDamageControl(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+
+ // Add new damage component
+ if (a.classList.contains("add-damage")) {
+ await this._onSubmit(event); // Submit any unsaved changes
+ const damage = this.item.data.data.damage;
+ return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])});
+ }
+
+ // Remove a damage component
+ if (a.classList.contains("delete-damage")) {
+ await this._onSubmit(event); // Submit any unsaved changes
+ const li = a.closest(".damage-part");
+ const damage = foundry.utils.deepClone(this.item.data.data.damage);
+ damage.parts.splice(Number(li.dataset.damagePart), 1);
+ return this.item.update({"data.damage.parts": damage.parts});
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application for selection various options.
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onConfigureTraits(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+
+ const options = {
+ name: a.dataset.target,
+ title: a.parentElement.innerText,
+ choices: [],
+ allowCustom: false
+ };
+
+ switch (a.dataset.options) {
+ case "saves":
+ options.choices = CONFIG.SW5E.abilities;
+ options.valueKey = null;
+ break;
+ case "skills":
+ const skills = this.item.data.data.skills;
+ const choiceSet =
+ skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
+ options.choices = Object.fromEntries(
+ Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
+ );
+ options.maximum = skills.number;
+ break;
+ }
+ new TraitSelector(this.item, options).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @inheritdoc */
+ async _onSubmit(...args) {
+ if (this._tabs[0].active === "details") this.position.height = "auto";
+ await super._onSubmit(...args);
+ }
}
diff --git a/module/macros.js b/module/macros.js
index 96bb3419..e8919864 100644
--- a/module/macros.js
+++ b/module/macros.js
@@ -1,4 +1,3 @@
-
/* -------------------------------------------- */
/* Hotbar Macros */
/* -------------------------------------------- */
@@ -11,24 +10,24 @@
* @returns {Promise}
*/
export async function create5eMacro(data, slot) {
- if ( data.type !== "Item" ) return;
- if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
- const item = data.data;
+ if (data.type !== "Item") return;
+ if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
+ const item = data.data;
- // Create the macro command
- const command = `game.sw5e.rollItemMacro("${item.name}");`;
- let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
- if ( !macro ) {
- macro = await Macro.create({
- name: item.name,
- type: "script",
- img: item.img,
- command: command,
- flags: {"sw5e.itemMacro": true}
- });
- }
- game.user.assignHotbarMacro(macro, slot);
- return false;
+ // Create the macro command
+ const command = `game.sw5e.rollItemMacro("${item.name}");`;
+ let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
+ if (!macro) {
+ macro = await Macro.create({
+ name: item.name,
+ type: "script",
+ img: item.img,
+ command: command,
+ flags: {"sw5e.itemMacro": true}
+ });
+ }
+ game.user.assignHotbarMacro(macro, slot);
+ return false;
}
/* -------------------------------------------- */
@@ -40,20 +39,22 @@ export async function create5eMacro(data, slot) {
* @return {Promise}
*/
export function rollItemMacro(itemName) {
- const speaker = ChatMessage.getSpeaker();
- let actor;
- if ( speaker.token ) actor = game.actors.tokens[speaker.token];
- if ( !actor ) actor = game.actors.get(speaker.actor);
+ const speaker = ChatMessage.getSpeaker();
+ let actor;
+ if (speaker.token) actor = game.actors.tokens[speaker.token];
+ if (!actor) actor = game.actors.get(speaker.actor);
- // Get matching items
- const items = actor ? actor.items.filter(i => i.name === itemName) : [];
- if ( items.length > 1 ) {
- ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
- } else if ( items.length === 0 ) {
- return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
- }
- const item = items[0];
+ // Get matching items
+ const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
+ if (items.length > 1) {
+ ui.notifications.warn(
+ `Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
+ );
+ } else if (items.length === 0) {
+ return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
+ }
+ const item = items[0];
- // Trigger the item roll
- return item.roll();
+ // Trigger the item roll
+ return item.roll();
}
diff --git a/module/migration.js b/module/migration.js
index e549e934..ab6eb420 100644
--- a/module/migration.js
+++ b/module/migration.js
@@ -2,65 +2,68 @@
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
* @return {Promise} A Promise which resolves once the migration is completed
*/
-export const migrateWorld = async function() {
- ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
+export const migrateWorld = async function () {
+ ui.notifications.info(
+ `Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`,
+ {permanent: true}
+ );
- // Migrate World Actors
- for await ( let a of game.actors.contents ) {
- try {
- console.log(`Checking Actor entity ${a.name} for migration needs`);
- const updateData = await migrateActorData(a.data);
- if ( !foundry.utils.isObjectEmpty(updateData) ) {
- console.log(`Migrating Actor entity ${a.name}`);
- await a.update(updateData, {enforceTypes: false});
- }
- } catch(err) {
- err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
- console.error(err);
+ // Migrate World Actors
+ for await (let a of game.actors.contents) {
+ try {
+ console.log(`Checking Actor entity ${a.name} for migration needs`);
+ const updateData = await migrateActorData(a.data);
+ if (!foundry.utils.isObjectEmpty(updateData)) {
+ console.log(`Migrating Actor entity ${a.name}`);
+ await a.update(updateData, {enforceTypes: false});
+ }
+ } catch (err) {
+ err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
+ console.error(err);
+ }
}
- }
- // Migrate World Items
- for ( let i of game.items.contents ) {
- try {
- const updateData = migrateItemData(i.toObject());
- if ( !foundry.utils.isObjectEmpty(updateData) ) {
- console.log(`Migrating Item entity ${i.name}`);
- await i.update(updateData, {enforceTypes: false});
- }
- } catch(err) {
- err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
- console.error(err);
+ // Migrate World Items
+ for (let i of game.items.contents) {
+ try {
+ const updateData = migrateItemData(i.toObject());
+ if (!foundry.utils.isObjectEmpty(updateData)) {
+ console.log(`Migrating Item entity ${i.name}`);
+ await i.update(updateData, {enforceTypes: false});
+ }
+ } catch (err) {
+ err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
+ console.error(err);
+ }
}
- }
- // Migrate Actor Override Tokens
- for ( let s of game.scenes.contents ) {
- try {
- const updateData = await migrateSceneData(s.data);
- if ( !foundry.utils.isObjectEmpty(updateData) ) {
- console.log(`Migrating Scene entity ${s.name}`);
- await s.update(updateData, {enforceTypes: false});
- // If we do not do this, then synthetic token actors remain in cache
- // with the un-updated actorData.
- s.tokens.contents.forEach(t => t._actor = null);
- }
- } catch(err) {
- err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
- console.error(err);
+ // Migrate Actor Override Tokens
+ for (let s of game.scenes.contents) {
+ try {
+ const updateData = await migrateSceneData(s.data);
+ if (!foundry.utils.isObjectEmpty(updateData)) {
+ console.log(`Migrating Scene entity ${s.name}`);
+ await s.update(updateData, {enforceTypes: false});
+ // If we do not do this, then synthetic token actors remain in cache
+ // with the un-updated actorData.
+ s.tokens.contents.forEach((t) => (t._actor = null));
+ }
+ } catch (err) {
+ err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
+ console.error(err);
+ }
}
- }
- // Migrate World Compendium 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);
- }
+ // Migrate World Compendium 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);
+ }
- // Set the migration as complete
- game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
- ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
+ // Set the migration as complete
+ game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
+ ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
};
/* -------------------------------------------- */
@@ -70,50 +73,48 @@ export const migrateWorld = async function() {
* @param pack
* @return {Promise}
*/
-export const migrateCompendium = async function(pack) {
- const entity = pack.metadata.entity;
- if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
+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});
+ // 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 documents = await pack.getDocuments();
+ // Begin by requesting server-side data model migration and get the migrated content
+ await pack.migrate();
+ const documents = await pack.getDocuments();
- // Iterate over compendium entries - applying fine-tuned migration functions
- for await ( let doc of documents ) {
- let updateData = {};
- try {
- switch (entity) {
- case "Actor":
- updateData = await migrateActorData(doc.data);
- break;
- case "Item":
- updateData = migrateItemData(doc.toObject());
- break;
- case "Scene":
- updateData = await migrateSceneData(doc.data);
- break;
- }
- if ( foundry.utils.isObjectEmpty(updateData) ) continue;
+ // Iterate over compendium entries - applying fine-tuned migration functions
+ for await (let doc of documents) {
+ let updateData = {};
+ try {
+ switch (entity) {
+ case "Actor":
+ updateData = await migrateActorData(doc.data);
+ break;
+ case "Item":
+ updateData = migrateItemData(doc.toObject());
+ break;
+ case "Scene":
+ updateData = await migrateSceneData(doc.data);
+ break;
+ }
+ if (foundry.utils.isObjectEmpty(updateData)) continue;
- // Save the entry, if data was changed
- await doc.update(updateData);
- console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
+ // Save the entry, if data was changed
+ await doc.update(updateData);
+ console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
+ } catch (err) {
+ // Handle migration failures
+ err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
+ console.error(err);
+ }
}
- // Handle migration failures
- catch(err) {
- err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
- console.error(err);
- }
- }
-
- // Apply the original locked status for the pack
- await pack.configure({locked: wasLocked});
- console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
+ // Apply the original locked status for the pack
+ await pack.configure({locked: wasLocked});
+ console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
};
/* -------------------------------------------- */
@@ -126,84 +127,82 @@ export const migrateCompendium = async function(pack) {
* @param {object} actor The actor data object to update
* @return {Object} The updateData to apply
*/
-export const migrateActorData = async function(actor) {
- const updateData = {};
+export const migrateActorData = async function (actor) {
+ const updateData = {};
- // Actor Data Updates
- if(actor.data) {
- _migrateActorMovement(actor, updateData);
- _migrateActorSenses(actor, updateData);
- _migrateActorType(actor, updateData);
- }
+ // Actor Data Updates
+ if (actor.data) {
+ _migrateActorMovement(actor, updateData);
+ _migrateActorSenses(actor, updateData);
+ _migrateActorType(actor, updateData);
+ }
- // Migrate Owned Items
- if ( !!actor.items ) {
- const items = await actor.items.reduce(async (memo, i) => {
- const results = await memo;
+ // Migrate Owned Items
+ if (!!actor.items) {
+ const items = await actor.items.reduce(async (memo, i) => {
+ const results = await memo;
- // Migrate the Owned Item
- const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
- let itemUpdate = await migrateActorItemData(itemData, actor);
+ // Migrate the Owned Item
+ const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
+ let itemUpdate = await migrateActorItemData(itemData, actor);
- // Prepared, Equipped, and Proficient for NPC actors
- if ( actor.type === "npc" ) {
- if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
- if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
- if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
- }
+ // Prepared, Equipped, and Proficient for NPC actors
+ if (actor.type === "npc") {
+ if (getProperty(itemData.data, "preparation.prepared") === false)
+ itemUpdate["data.preparation.prepared"] = true;
+ if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
+ if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
+ }
- // Update the Owned Item
- if ( !isObjectEmpty(itemUpdate) ) {
- itemUpdate._id = itemData._id;
- console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
- results.push(expandObject(itemUpdate));
- }
+ // Update the Owned Item
+ if (!isObjectEmpty(itemUpdate)) {
+ itemUpdate._id = itemData._id;
+ console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
+ results.push(expandObject(itemUpdate));
+ }
- return results;
- }, []);
+ return results;
+ }, []);
- if ( items.length > 0 ) updateData.items = items;
- }
+ if (items.length > 0) updateData.items = items;
+ }
- // Update NPC data with new datamodel information
- if (actor.type === "npc") {
- _updateNPCData(actor);
- }
+ // Update NPC data with new datamodel information
+ if (actor.type === "npc") {
+ _updateNPCData(actor);
+ }
- // migrate powers last since it relies on item classes being migrated first.
- _migrateActorPowers(actor, updateData);
-
- return updateData;
+ // migrate powers last since it relies on item classes being migrated first.
+ _migrateActorPowers(actor, updateData);
+
+ 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 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);
+ }
- // 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;
+ // Return the scrubbed data
+ return actorData;
}
-
/* -------------------------------------------- */
/**
@@ -212,11 +211,11 @@ function cleanActorData(actorData) {
* @param {object} item Item data to migrate
* @return {object} The updateData to apply
*/
-export const migrateItemData = function(item) {
- const updateData = {};
- _migrateItemClassPowerCasting(item, updateData);
- _migrateItemAttunement(item, updateData);
- return updateData;
+export const migrateItemData = function (item) {
+ const updateData = {};
+ _migrateItemClassPowerCasting(item, updateData);
+ _migrateItemAttunement(item, updateData);
+ return updateData;
};
/* -------------------------------------------- */
@@ -226,12 +225,12 @@ export const migrateItemData = function(item) {
* @param item
* @param actor
*/
-export const migrateActorItemData = async function(item, actor) {
- const updateData = {};
- _migrateItemClassPowerCasting(item, updateData);
- _migrateItemAttunement(item, updateData);
- await _migrateItemPower(item, actor, updateData);
- return updateData;
+export const migrateActorItemData = async function (item, actor) {
+ const updateData = {};
+ _migrateItemClassPowerCasting(item, updateData);
+ _migrateItemAttunement(item, updateData);
+ await _migrateItemPower(item, actor, updateData);
+ return updateData;
};
/* -------------------------------------------- */
@@ -242,33 +241,34 @@ export const migrateActorItemData = async function(item, actor) {
* @param {Object} scene The Scene data to Update
* @return {Object} The updateData to apply
*/
- export const migrateSceneData = async function(scene) {
- const tokens = await Promise.all(scene.tokens.map(async token => {
- const t = token.toJSON();
- if (!t.actorId || t.actorLink) {
- t.actorData = {};
- }
- else if (!game.actors.has(t.actorId)) {
- t.actorId = null;
- t.actorData = {};
- } else if ( !t.actorLink ) {
- const actorData = duplicate(t.actorData);
- actorData.type = token.actor?.type;
- const update = migrateActorData(actorData);
- ['items', 'effects'].forEach(embeddedName => {
- if (!update[embeddedName]?.length) return;
- const updates = new Map(update[embeddedName].map(u => [u._id, u]));
- t.actorData[embeddedName].forEach(original => {
- const update = updates.get(original._id);
- if (update) mergeObject(original, update);
- });
- delete update[embeddedName];
- });
+export const migrateSceneData = async function (scene) {
+ const tokens = await Promise.all(
+ scene.tokens.map(async (token) => {
+ const t = token.toJSON();
+ if (!t.actorId || t.actorLink) {
+ t.actorData = {};
+ } else if (!game.actors.has(t.actorId)) {
+ t.actorId = null;
+ t.actorData = {};
+ } else if (!t.actorLink) {
+ const actorData = duplicate(t.actorData);
+ actorData.type = token.actor?.type;
+ const update = migrateActorData(actorData);
+ ["items", "effects"].forEach((embeddedName) => {
+ if (!update[embeddedName]?.length) return;
+ const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
+ t.actorData[embeddedName].forEach((original) => {
+ const update = updates.get(original._id);
+ if (update) mergeObject(original, update);
+ });
+ delete update[embeddedName];
+ });
- mergeObject(t.actorData, update);
- }
- return t;
- }));
+ mergeObject(t.actorData, update);
+ }
+ return t;
+ })
+ );
return {tokens};
};
@@ -284,87 +284,93 @@ export const migrateActorItemData = async function(item, actor) {
* @return {Object} The updated Actor
*/
function _updateNPCData(actor) {
+ let actorData = actor.data;
+ const updateData = {};
+ // check for flag.core, if not there is no compendium monster so exit
+ const hasSource = actor?.flags?.core?.sourceId !== undefined;
+ if (!hasSource) return actor;
+ // shortcut out if dataVersion flag is set to 1.2.4 or higher
+ const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
+ if (
+ hasDataVersion &&
+ (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))
+ )
+ return actor;
+ // Check to see what the source of NPC is
+ const sourceId = actor.flags.core.sourceId;
+ const coreSource = sourceId.substr(0, sourceId.length - 17);
+ const core_id = sourceId.substr(sourceId.length - 16, 16);
+ if (coreSource === "Compendium.sw5e.monsters") {
+ game.packs
+ .get("sw5e.monsters")
+ .getEntity(core_id)
+ .then((monster) => {
+ const monsterData = monster.data.data;
+ // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
+ updateData["data.attributes.movement"] = monsterData.attributes.movement;
+ updateData["data.attributes.senses"] = monsterData.attributes.senses;
+ updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting;
+ updateData["data.attributes.force"] = monsterData.attributes.force;
+ updateData["data.attributes.tech"] = monsterData.attributes.tech;
+ updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel;
+ updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
+ // push missing powers onto actor
+ let newPowers = [];
+ for (let i of monster.items) {
+ const itemData = i.data;
+ if (itemData.type === "power") {
+ const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
+ let hasPower = !!actor.items.find(
+ (item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
+ );
+ if (!hasPower) {
+ // Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
+ const newPower = JSON.parse(JSON.stringify(itemData));
- let actorData = actor.data;
- const updateData = {};
- // check for flag.core, if not there is no compendium monster so exit
- const hasSource = actor?.flags?.core?.sourceId !== undefined;
- if (!hasSource) return actor;
- // shortcut out if dataVersion flag is set to 1.2.4 or higher
- const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
- if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor;
- // Check to see what the source of NPC is
- const sourceId = actor.flags.core.sourceId;
- const coreSource = sourceId.substr(0,sourceId.length-17);
- const core_id = sourceId.substr(sourceId.length-16,16);
- if (coreSource === "Compendium.sw5e.monsters"){
- game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => {
- const monsterData = monster.data.data;
- // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
- updateData["data.attributes.movement"] = monsterData.attributes.movement;
- updateData["data.attributes.senses"] = monsterData.attributes.senses;
- updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting;
- updateData["data.attributes.force"] = monsterData.attributes.force;
- updateData["data.attributes.tech"] = monsterData.attributes.tech;
- updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel;
- updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
- // push missing powers onto actor
- let newPowers = [];
- for ( let i of monster.items ) {
- const itemData = i.data;
- if ( itemData.type === "power" ) {
- const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
- let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id);
- if (!hasPower) {
- // Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
- const newPower = JSON.parse(JSON.stringify(itemData));
+ newPowers.push(newPower);
+ }
+ }
+ }
- newPowers.push(newPower);
- }
- }
- }
+ // get actor to create new powers
+ const liveActor = game.actors.get(actor._id);
+ // create the powers on the actor
+ liveActor.createEmbeddedEntity("OwnedItem", newPowers);
- // get actor to create new powers
- const liveActor = game.actors.get(actor._id);
- // create the powers on the actor
- liveActor.createEmbeddedEntity("OwnedItem", newPowers);
+ // set flag to check to see if migration has been done so we don't do it again.
+ liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
+ });
+ }
- // set flag to check to see if migration has been done so we don't do it again.
- liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
- })
- }
-
-
- //merge object
- actorData = mergeObject(actorData, updateData);
- // Return the scrubbed data
- return actor;
+ //merge object
+ actorData = mergeObject(actorData, updateData);
+ // Return the scrubbed data
+ return actor;
}
-
/**
* Migrate the actor speed string to movement object
* @private
*/
function _migrateActorMovement(actorData, updateData) {
- const ad = actorData.data;
+ const ad = actorData.data;
- // Work is needed if old data is present
- const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
- const hasOld = old !== undefined;
- if ( hasOld ) {
+ // Work is needed if old data is present
+ const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
+ const hasOld = old !== undefined;
+ if (hasOld) {
+ // If new data is not present, migrate the old data
+ const hasNew = ad?.attributes?.movement?.walk !== undefined;
+ if (!hasNew && typeof old === "string") {
+ const s = (old || "").split(" ");
+ if (s.length > 0)
+ updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
+ }
- // If new data is not present, migrate the old data
- const hasNew = ad?.attributes?.movement?.walk !== undefined;
- if ( !hasNew && (typeof old === "string") ) {
- const s = (old || "").split(" ");
- if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
+ // Remove the old attribute
+ updateData["data.attributes.-=speed"] = null;
}
-
- // Remove the old attribute
- updateData["data.attributes.-=speed"] = null;
- }
- return updateData
+ return updateData;
}
/* -------------------------------------------- */
@@ -374,58 +380,58 @@ function _migrateActorMovement(actorData, updateData) {
* @private
*/
function _migrateActorPowers(actorData, updateData) {
- const ad = actorData.data;
+ const ad = actorData.data;
- // If new Force & Tech data is not present, create it
- let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
- if ( !hasNewAttrib ) {
- updateData["data.attributes.force.known.value"] = 0;
- updateData["data.attributes.force.known.max"] = 0;
- updateData["data.attributes.force.points.value"] = 0;
- updateData["data.attributes.force.points.min"] = 0;
- updateData["data.attributes.force.points.max"] = 0;
- updateData["data.attributes.force.points.temp"] = null;
- updateData["data.attributes.force.points.tempmax"] = null;
- updateData["data.attributes.force.level"] = 0;
- updateData["data.attributes.tech.known.value"] = 0;
- updateData["data.attributes.tech.known.max"] = 0;
- updateData["data.attributes.tech.points.value"] = 0;
- updateData["data.attributes.tech.points.min"] = 0;
- updateData["data.attributes.tech.points.max"] = 0;
- updateData["data.attributes.tech.points.temp"] = null;
- updateData["data.attributes.tech.points.tempmax"] = null;
- updateData["data.attributes.tech.level"] = 0;
- }
-
- // If new Power F/T split data is not present, create it
- const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
- if ( !hasNewLimit ) {
- for (let i = 1; i <= 9; i++) {
- // add new
- updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers,"power" + i + ".value");
- updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers,"power" + i + ".max");
- updateData["data.powers.power" + i + ".foverride"] = null;
- updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers,"power" + i + ".value");
- updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers,"power" + i + ".max");
- updateData["data.powers.power" + i + ".toverride"] = null;
- //remove old
- updateData["data.powers.power" + i + ".-=value"] = null;
- updateData["data.powers.power" + i + ".-=override"] = null;
+ // If new Force & Tech data is not present, create it
+ let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
+ if (!hasNewAttrib) {
+ updateData["data.attributes.force.known.value"] = 0;
+ updateData["data.attributes.force.known.max"] = 0;
+ updateData["data.attributes.force.points.value"] = 0;
+ updateData["data.attributes.force.points.min"] = 0;
+ updateData["data.attributes.force.points.max"] = 0;
+ updateData["data.attributes.force.points.temp"] = null;
+ updateData["data.attributes.force.points.tempmax"] = null;
+ updateData["data.attributes.force.level"] = 0;
+ updateData["data.attributes.tech.known.value"] = 0;
+ updateData["data.attributes.tech.known.max"] = 0;
+ updateData["data.attributes.tech.points.value"] = 0;
+ updateData["data.attributes.tech.points.min"] = 0;
+ updateData["data.attributes.tech.points.max"] = 0;
+ updateData["data.attributes.tech.points.temp"] = null;
+ updateData["data.attributes.tech.points.tempmax"] = null;
+ updateData["data.attributes.tech.level"] = 0;
}
- }
- // If new Bonus Power DC data is not present, create it
- const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
- if ( !hasNewBonus ) {
- updateData["data.bonuses.power.forceLightDC"] = "";
- updateData["data.bonuses.power.forceDarkDC"] = "";
- updateData["data.bonuses.power.forceUnivDC"] = "";
- updateData["data.bonuses.power.techDC"] = "";
- }
- // Remove the Power DC Bonus
- updateData["data.bonuses.power.-=dc"] = null;
+ // If new Power F/T split data is not present, create it
+ const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
+ if (!hasNewLimit) {
+ for (let i = 1; i <= 9; i++) {
+ // add new
+ updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value");
+ updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max");
+ updateData["data.powers.power" + i + ".foverride"] = null;
+ updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value");
+ updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max");
+ updateData["data.powers.power" + i + ".toverride"] = null;
+ //remove old
+ updateData["data.powers.power" + i + ".-=value"] = null;
+ updateData["data.powers.power" + i + ".-=override"] = null;
+ }
+ }
+ // If new Bonus Power DC data is not present, create it
+ const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
+ if (!hasNewBonus) {
+ updateData["data.bonuses.power.forceLightDC"] = "";
+ updateData["data.bonuses.power.forceDarkDC"] = "";
+ updateData["data.bonuses.power.forceUnivDC"] = "";
+ updateData["data.bonuses.power.techDC"] = "";
+ }
- return updateData
+ // Remove the Power DC Bonus
+ updateData["data.bonuses.power.-=dc"] = null;
+
+ return updateData;
}
/* -------------------------------------------- */
@@ -435,35 +441,35 @@ function _migrateActorPowers(actorData, updateData) {
* @private
*/
function _migrateActorSenses(actor, updateData) {
- const ad = actor.data;
- if ( ad?.traits?.senses === undefined ) return;
- const original = ad.traits.senses || "";
- if ( typeof original !== "string" ) return;
+ const ad = actor.data;
+ if (ad?.traits?.senses === undefined) return;
+ const original = ad.traits.senses || "";
+ if (typeof original !== "string") return;
- // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
- const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
- let wasMatched = false;
+ // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
+ const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
+ let wasMatched = false;
- // Match each comma-separated term
- for ( let s of original.split(",") ) {
- s = s.trim();
- const match = s.match(pattern);
- if ( !match ) continue;
- const type = match[1].toLowerCase();
- if ( type in CONFIG.SW5E.senses ) {
- updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
- wasMatched = true;
+ // Match each comma-separated term
+ for (let s of original.split(",")) {
+ s = s.trim();
+ const match = s.match(pattern);
+ if (!match) continue;
+ const type = match[1].toLowerCase();
+ if (type in CONFIG.SW5E.senses) {
+ updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
+ wasMatched = true;
+ }
}
- }
- // If nothing was matched, but there was an old string - put the whole thing in "special"
- if ( !wasMatched && !!original ) {
- updateData["data.attributes.senses.special"] = original;
- }
+ // If nothing was matched, but there was an old string - put the whole thing in "special"
+ if (!wasMatched && !!original) {
+ updateData["data.attributes.senses.special"] = original;
+ }
- // Remove the old traits.senses string once the migration is complete
- updateData["data.traits.-=senses"] = null;
- return updateData;
+ // Remove the old traits.senses string once the migration is complete
+ updateData["data.traits.-=senses"] = null;
+ return updateData;
}
/* -------------------------------------------- */
@@ -473,76 +479,77 @@ function _migrateActorSenses(actor, updateData) {
* @private
*/
function _migrateActorType(actor, updateData) {
- const ad = actor.data;
- const original = ad.details?.type;
- if ( typeof original !== "string" ) return;
+ const ad = actor.data;
+ const original = ad.details?.type;
+ if (typeof original !== "string") return;
- // New default data structure
- let data = {
- "value": "",
- "subtype": "",
- "swarm": "",
- "custom": ""
- }
+ // New default data structure
+ let data = {
+ value: "",
+ subtype: "",
+ swarm: "",
+ custom: ""
+ };
- // Specifics
- // (Some of these have weird names, these need to be addressed individually)
- if (original === "force entity") {
- data.value = "force";
- data.subtype = "storm";
- } else if (original === "human") {
- data.value = "humanoid";
- data.subtype = "human";
- } else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) {
- data.value = "humanoid";
- } else if (original === "tree") {
- data.value = "plant";
- data.subtype = "tree";
- } else if (original === "(humanoid) or Large (beast) force entity") {
- data.value = "force";
- } else if (original === "droid (appears human)") {
- data.value = "droid";
- } else {
- // Match the existing string
- const pattern = /^(?:swarm of (?[\w\-]+) )?(?[^(]+?)(?:\((?[^)]+)\))?$/i;
- const match = original.trim().match(pattern);
- if (match) {
+ // Specifics
+ // (Some of these have weird names, these need to be addressed individually)
+ if (original === "force entity") {
+ data.value = "force";
+ data.subtype = "storm";
+ } else if (original === "human") {
+ data.value = "humanoid";
+ data.subtype = "human";
+ } else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) {
+ data.value = "humanoid";
+ } else if (original === "tree") {
+ data.value = "plant";
+ data.subtype = "tree";
+ } else if (original === "(humanoid) or Large (beast) force entity") {
+ data.value = "force";
+ } else if (original === "droid (appears human)") {
+ data.value = "droid";
+ } else {
+ // Match the existing string
+ const pattern = /^(?:swarm of (?[\w\-]+) )?(?[^(]+?)(?:\((?[^)]+)\))?$/i;
+ const match = original.trim().match(pattern);
+ if (match) {
+ // Match a known creature type
+ const typeLc = match.groups.type.trim().toLowerCase();
+ const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
+ return (
+ typeLc === k ||
+ typeLc === game.i18n.localize(v).toLowerCase() ||
+ typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()
+ );
+ });
+ if (typeMatch) data.value = typeMatch[0];
+ else {
+ data.value = "custom";
+ data.custom = match.groups.type.trim().titleCase();
+ }
+ data.subtype = match.groups.subtype?.trim().titleCase() || "";
- // Match a known creature type
- const typeLc = match.groups.type.trim().toLowerCase();
- const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
- return (typeLc === k) ||
- (typeLc === game.i18n.localize(v).toLowerCase()) ||
- (typeLc === game.i18n.localize(`${v}Pl`).toLowerCase());
- });
- if (typeMatch) data.value = typeMatch[0];
- else {
- data.value = "custom";
- data.custom = match.groups.type.trim().titleCase();
- }
- data.subtype = match.groups.subtype?.trim().titleCase() || "";
+ // Match a swarm
+ const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm"));
+ if (match.groups.size || isNamedSwarm) {
+ const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
+ const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
+ return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase();
+ });
+ data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
+ } else data.swarm = "";
+ }
- // Match a swarm
- const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm"));
- if (match.groups.size || isNamedSwarm) {
- const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
- const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
- return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase());
- });
- data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
- } else data.swarm = "";
+ // No match found
+ else {
+ data.value = "custom";
+ data.custom = original;
+ }
}
- // No match found
- else {
- data.value = "custom";
- data.custom = original;
- }
- }
-
- // Update the actor data
- updateData["data.details.type"] = data;
- return updateData;
+ // Update the actor data
+ updateData["data.details.type"] = data;
+ return updateData;
}
/* -------------------------------------------- */
@@ -551,42 +558,41 @@ function _migrateActorType(actor, updateData) {
* @private
*/
function _migrateItemClassPowerCasting(item, updateData) {
- if (item.type === "class"){
- switch (item.name){
- case "Consular":
- updateData["data.powercasting"] = {
- progression: "consular",
- ability: ""
- };
- break;
- case "Engineer":
-
- updateData["data.powercasting"] = {
- progression: "engineer",
- ability: ""
- };
- break;
- case "Guardian":
- updateData["data.powercasting"] = {
- progression: "guardian",
- ability: ""
- };
- break;
- case "Scout":
- updateData["data.powercasting"] = {
- progression: "scout",
- ability: ""
- };
- break;
- case "Sentinel":
- updateData["data.powercasting"] = {
- progression: "sentinel",
- ability: ""
- };
- break;
+ if (item.type === "class") {
+ switch (item.name) {
+ case "Consular":
+ updateData["data.powercasting"] = {
+ progression: "consular",
+ ability: ""
+ };
+ break;
+ case "Engineer":
+ updateData["data.powercasting"] = {
+ progression: "engineer",
+ ability: ""
+ };
+ break;
+ case "Guardian":
+ updateData["data.powercasting"] = {
+ progression: "guardian",
+ ability: ""
+ };
+ break;
+ case "Scout":
+ updateData["data.powercasting"] = {
+ progression: "scout",
+ ability: ""
+ };
+ break;
+ case "Sentinel":
+ updateData["data.powercasting"] = {
+ progression: "sentinel",
+ ability: ""
+ };
+ break;
+ }
}
- }
- return updateData;
+ return updateData;
}
/* -------------------------------------------- */
@@ -598,42 +604,45 @@ function _migrateItemClassPowerCasting(item, updateData) {
* @private
*/
async function _migrateItemPower(item, actor, updateData) {
- // if item is not a power shortcut out
- if (item.type !== "power") return updateData;
+ // if item is not a power shortcut out
+ if (item.type !== "power") return updateData;
- console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`);
- // check for flag.core, if not there is no compendium power so exit
- const hasSource = item?.flags?.core?.sourceId !== undefined;
- if (!hasSource) return updateData;
+ console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`);
+ // check for flag.core, if not there is no compendium power so exit
+ const hasSource = item?.flags?.core?.sourceId !== undefined;
+ if (!hasSource) return updateData;
- // shortcut out if dataVersion flag is set to 1.2.4 or higher
- const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
- if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData;
-
- // Check to see what the source of Power is
- const sourceId = item.flags.core.sourceId;
- const coreSource = sourceId.substr(0, sourceId.length - 17);
- const core_id = sourceId.substr(sourceId.length - 16, 16);
-
- //if power type is not force or tech exit out
- let powerType = "none";
- if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers";
- if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers";
- if (powerType === "none") return updateData;
+ // shortcut out if dataVersion flag is set to 1.2.4 or higher
+ const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
+ if (
+ hasDataVersion &&
+ (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))
+ )
+ return updateData;
+
+ // Check to see what the source of Power is
+ const sourceId = item.flags.core.sourceId;
+ const coreSource = sourceId.substr(0, sourceId.length - 17);
+ const core_id = sourceId.substr(sourceId.length - 16, 16);
+
+ //if power type is not force or tech exit out
+ let powerType = "none";
+ if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers";
+ if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers";
+ if (powerType === "none") return updateData;
const corePower = duplicate(await game.packs.get(powerType).getEntity(core_id));
console.log(`Updating Actor ${actor.name}'s ${item.name} from compendium`);
const corePowerData = corePower.data;
// copy Core Power Data over original Power
updateData["data"] = corePowerData;
- updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}};
+ updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}};
return updateData;
-
-
- //game.packs.get(powerType).getEntity(core_id).then(corePower => {
- //})
+ //game.packs.get(powerType).getEntity(core_id).then(corePower => {
+
+ //})
}
/* -------------------------------------------- */
@@ -647,10 +656,10 @@ async function _migrateItemPower(item, actor, updateData) {
* @private
*/
function _migrateItemAttunement(item, updateData) {
- if ( item.data?.attuned === undefined ) return updateData;
- updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
- updateData["data.-=attuned"] = null;
- return updateData;
+ if (item.data?.attuned === undefined) return updateData;
+ updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
+ updateData["data.-=attuned"] = null;
+ return updateData;
}
/* -------------------------------------------- */
@@ -661,43 +670,41 @@ function _migrateItemAttunement(item, updateData) {
* @private
*/
export async function purgeFlags(pack) {
- const cleanFlags = (flags) => {
- const flags5e = flags.sw5e || null;
- return flags5e ? {sw5e: flags5e} : {};
- };
- await pack.configure({locked: false});
- const content = await pack.getContent();
- for ( let entity of content ) {
- const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
- if ( pack.entity === "Actor" ) {
- update.items = entity.data.items.map(i => {
- i.flags = cleanFlags(i.flags);
- return i;
- })
+ const cleanFlags = (flags) => {
+ const flags5e = flags.sw5e || null;
+ return flags5e ? {sw5e: flags5e} : {};
+ };
+ await pack.configure({locked: false});
+ const content = await pack.getContent();
+ for (let entity of content) {
+ const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
+ if (pack.entity === "Actor") {
+ update.items = entity.data.items.map((i) => {
+ i.flags = cleanFlags(i.flags);
+ return i;
+ });
+ }
+ await pack.updateEntity(update, {recursive: false});
+ console.log(`Purged flags from ${entity.name}`);
}
- await pack.updateEntity(update, {recursive: false});
- console.log(`Purged flags from ${entity.name}`);
- }
- await pack.configure({locked: true});
+ 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);
+ 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;
+ return data;
}
diff --git a/module/pixi/ability-template.js b/module/pixi/ability-template.js
index 3af99181..6799eaec 100644
--- a/module/pixi/ability-template.js
+++ b/module/pixi/ability-template.js
@@ -1,133 +1,132 @@
-import { SW5E } from "../config.js";
+import {SW5E} from "../config.js";
/**
* A helper class for building MeasuredTemplates for 5e powers and abilities
* @extends {MeasuredTemplate}
*/
export default class AbilityTemplate extends MeasuredTemplate {
+ /**
+ * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
+ * @param {Item5e} item The Item object for which to construct the template
+ * @return {AbilityTemplate|null} The template object, or null if the item does not produce a template
+ */
+ static fromItem(item) {
+ const target = getProperty(item.data, "data.target") || {};
+ const templateShape = SW5E.areaTargetTypes[target.type];
+ if (!templateShape) return null;
- /**
- * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
- * @param {Item5e} item The Item object for which to construct the template
- * @return {AbilityTemplate|null} The template object, or null if the item does not produce a template
- */
- static fromItem(item) {
- const target = getProperty(item.data, "data.target") || {};
- const templateShape = SW5E.areaTargetTypes[target.type];
- if ( !templateShape ) return null;
+ // Prepare template data
+ const templateData = {
+ t: templateShape,
+ user: game.user.data._id,
+ distance: target.value,
+ direction: 0,
+ x: 0,
+ y: 0,
+ fillColor: game.user.color
+ };
- // Prepare template data
- const templateData = {
- t: templateShape,
- user: game.user.data._id,
- distance: target.value,
- direction: 0,
- x: 0,
- y: 0,
- fillColor: game.user.color
- };
+ // Additional type-specific data
+ switch (templateShape) {
+ case "cone":
+ templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
+ break;
+ case "rect": // 5e rectangular AoEs are always cubes
+ templateData.distance = Math.hypot(target.value, target.value);
+ templateData.width = target.value;
+ templateData.direction = 45;
+ break;
+ case "ray": // 5e rays are most commonly 1 square (5 ft) in width
+ templateData.width = target.width ?? canvas.dimensions.distance;
+ break;
+ default:
+ break;
+ }
- // Additional type-specific data
- switch ( templateShape ) {
- case "cone":
- templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
- break;
- case "rect": // 5e rectangular AoEs are always cubes
- templateData.distance = Math.hypot(target.value, target.value);
- templateData.width = target.value;
- templateData.direction = 45;
- break;
- case "ray": // 5e rays are most commonly 1 square (5 ft) in width
- templateData.width = target.width ?? canvas.dimensions.distance;
- break;
- default:
- break;
+ // Return the template constructed from the item data
+ const cls = CONFIG.MeasuredTemplate.documentClass;
+ const template = new cls(templateData, {parent: canvas.scene});
+ const object = new this(template);
+ object.item = item;
+ object.actorSheet = item.actor?.sheet || null;
+ return object;
}
- // Return the template constructed from the item data
- const cls = CONFIG.MeasuredTemplate.documentClass;
- const template = new cls(templateData, {parent: canvas.scene});
- const object = new this(template);
- object.item = item;
- object.actorSheet = item.actor?.sheet || null;
- return object;
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Creates a preview of the power template
+ */
+ drawPreview() {
+ const initialLayer = canvas.activeLayer;
- /**
- * Creates a preview of the power template
- */
- drawPreview() {
- const initialLayer = canvas.activeLayer;
+ // Draw the template and switch to the template layer
+ this.draw();
+ this.layer.activate();
+ this.layer.preview.addChild(this);
- // Draw the template and switch to the template layer
- this.draw();
- this.layer.activate();
- this.layer.preview.addChild(this);
+ // Hide the sheet that originated the preview
+ if (this.actorSheet) this.actorSheet.minimize();
- // Hide the sheet that originated the preview
- if ( this.actorSheet ) this.actorSheet.minimize();
+ // Activate interactivity
+ this.activatePreviewListeners(initialLayer);
+ }
- // Activate interactivity
- this.activatePreviewListeners(initialLayer);
- }
+ /* -------------------------------------------- */
- /* -------------------------------------------- */
+ /**
+ * Activate listeners for the template preview
+ * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
+ */
+ activatePreviewListeners(initialLayer) {
+ const handlers = {};
+ let moveTime = 0;
- /**
- * Activate listeners for the template preview
- * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
- */
- activatePreviewListeners(initialLayer) {
- const handlers = {};
- let moveTime = 0;
+ // Update placement (mouse-move)
+ handlers.mm = (event) => {
+ event.stopPropagation();
+ let now = Date.now(); // Apply a 20ms throttle
+ if (now - moveTime <= 20) return;
+ const center = event.data.getLocalPosition(this.layer);
+ const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
+ this.data.update({x: snapped.x, y: snapped.y});
+ this.refresh();
+ moveTime = now;
+ };
- // Update placement (mouse-move)
- handlers.mm = event => {
- event.stopPropagation();
- let now = Date.now(); // Apply a 20ms throttle
- if ( now - moveTime <= 20 ) return;
- const center = event.data.getLocalPosition(this.layer);
- const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
- this.data.update({x: snapped.x, y: snapped.y});
- this.refresh();
- moveTime = now;
- };
+ // Cancel the workflow (right-click)
+ handlers.rc = (event) => {
+ this.layer.preview.removeChildren();
+ canvas.stage.off("mousemove", handlers.mm);
+ canvas.stage.off("mousedown", handlers.lc);
+ canvas.app.view.oncontextmenu = null;
+ canvas.app.view.onwheel = null;
+ initialLayer.activate();
+ this.actorSheet.maximize();
+ };
- // Cancel the workflow (right-click)
- handlers.rc = event => {
- this.layer.preview.removeChildren();
- canvas.stage.off("mousemove", handlers.mm);
- canvas.stage.off("mousedown", handlers.lc);
- canvas.app.view.oncontextmenu = null;
- canvas.app.view.onwheel = null;
- initialLayer.activate();
- this.actorSheet.maximize();
- };
+ // Confirm the workflow (left-click)
+ handlers.lc = (event) => {
+ handlers.rc(event);
+ const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
+ this.data.update(destination);
+ canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
+ };
- // Confirm the workflow (left-click)
- handlers.lc = event => {
- handlers.rc(event);
- const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
- this.data.update(destination);
- canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
- };
+ // Rotate the template by 3 degree increments (mouse-wheel)
+ handlers.mw = (event) => {
+ if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
+ event.stopPropagation();
+ let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
+ let snap = event.shiftKey ? delta : 5;
+ this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)});
+ this.refresh();
+ };
- // Rotate the template by 3 degree increments (mouse-wheel)
- handlers.mw = event => {
- if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
- event.stopPropagation();
- let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
- let snap = event.shiftKey ? delta : 5;
- this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))});
- this.refresh();
- };
-
- // Activate listeners
- canvas.stage.on("mousemove", handlers.mm);
- canvas.stage.on("mousedown", handlers.lc);
- canvas.app.view.oncontextmenu = handlers.rc;
- canvas.app.view.onwheel = handlers.mw;
- }
+ // Activate listeners
+ canvas.stage.on("mousemove", handlers.mm);
+ canvas.stage.on("mousedown", handlers.lc);
+ canvas.app.view.oncontextmenu = handlers.rc;
+ canvas.app.view.onwheel = handlers.mw;
+ }
}
diff --git a/module/settings.js b/module/settings.js
index 2adde8d3..3b765c69 100644
--- a/module/settings.js
+++ b/module/settings.js
@@ -1,145 +1,144 @@
-export const registerSystemSettings = function() {
+export const registerSystemSettings = function () {
+ /**
+ * Track the system version upon which point a migration was last applied
+ */
+ game.settings.register("sw5e", "systemMigrationVersion", {
+ name: "System Migration Version",
+ scope: "world",
+ config: false,
+ type: String,
+ default: game.system.data.version
+ });
- /**
- * Track the system version upon which point a migration was last applied
- */
- game.settings.register("sw5e", "systemMigrationVersion", {
- name: "System Migration Version",
- scope: "world",
- config: false,
- type: String,
- default: game.system.data.version
- });
+ /**
+ * Register resting variants
+ */
+ game.settings.register("sw5e", "restVariant", {
+ name: "SETTINGS.5eRestN",
+ hint: "SETTINGS.5eRestL",
+ scope: "world",
+ config: true,
+ default: "normal",
+ type: String,
+ choices: {
+ normal: "SETTINGS.5eRestPHB",
+ gritty: "SETTINGS.5eRestGritty",
+ epic: "SETTINGS.5eRestEpic"
+ }
+ });
- /**
- * Register resting variants
- */
- game.settings.register("sw5e", "restVariant", {
- name: "SETTINGS.5eRestN",
- hint: "SETTINGS.5eRestL",
- scope: "world",
- config: true,
- default: "normal",
- type: String,
- choices: {
- "normal": "SETTINGS.5eRestPHB",
- "gritty": "SETTINGS.5eRestGritty",
- "epic": "SETTINGS.5eRestEpic",
- }
- });
+ /**
+ * Register diagonal movement rule setting
+ */
+ game.settings.register("sw5e", "diagonalMovement", {
+ name: "SETTINGS.5eDiagN",
+ hint: "SETTINGS.5eDiagL",
+ scope: "world",
+ config: true,
+ default: "555",
+ type: String,
+ choices: {
+ 555: "SETTINGS.5eDiagPHB",
+ 5105: "SETTINGS.5eDiagDMG",
+ EUCL: "SETTINGS.5eDiagEuclidean"
+ },
+ onChange: (rule) => (canvas.grid.diagonalRule = rule)
+ });
- /**
- * Register diagonal movement rule setting
- */
- game.settings.register("sw5e", "diagonalMovement", {
- name: "SETTINGS.5eDiagN",
- hint: "SETTINGS.5eDiagL",
- scope: "world",
- config: true,
- default: "555",
- type: String,
- choices: {
- "555": "SETTINGS.5eDiagPHB",
- "5105": "SETTINGS.5eDiagDMG",
- "EUCL": "SETTINGS.5eDiagEuclidean",
- },
- onChange: rule => canvas.grid.diagonalRule = rule
- });
+ /**
+ * Register Initiative formula setting
+ */
+ game.settings.register("sw5e", "initiativeDexTiebreaker", {
+ name: "SETTINGS.5eInitTBN",
+ hint: "SETTINGS.5eInitTBL",
+ scope: "world",
+ config: true,
+ default: false,
+ type: Boolean
+ });
- /**
- * Register Initiative formula setting
- */
- game.settings.register("sw5e", "initiativeDexTiebreaker", {
- name: "SETTINGS.5eInitTBN",
- hint: "SETTINGS.5eInitTBL",
- scope: "world",
- config: true,
- default: false,
- type: Boolean
- });
+ /**
+ * Require Currency Carrying Weight
+ */
+ game.settings.register("sw5e", "currencyWeight", {
+ name: "SETTINGS.5eCurWtN",
+ hint: "SETTINGS.5eCurWtL",
+ scope: "world",
+ config: true,
+ default: true,
+ type: Boolean
+ });
- /**
- * Require Currency Carrying Weight
- */
- game.settings.register("sw5e", "currencyWeight", {
- name: "SETTINGS.5eCurWtN",
- hint: "SETTINGS.5eCurWtL",
- scope: "world",
- config: true,
- default: true,
- type: Boolean
- });
+ /**
+ * Option to disable XP bar for session-based or story-based advancement.
+ */
+ game.settings.register("sw5e", "disableExperienceTracking", {
+ name: "SETTINGS.5eNoExpN",
+ hint: "SETTINGS.5eNoExpL",
+ scope: "world",
+ config: true,
+ default: false,
+ type: Boolean
+ });
- /**
- * Option to disable XP bar for session-based or story-based advancement.
- */
- game.settings.register("sw5e", "disableExperienceTracking", {
- name: "SETTINGS.5eNoExpN",
- hint: "SETTINGS.5eNoExpL",
- scope: "world",
- config: true,
- default: false,
- type: Boolean,
- });
+ /**
+ * Option to automatically collapse Item Card descriptions
+ */
+ game.settings.register("sw5e", "autoCollapseItemCards", {
+ name: "SETTINGS.5eAutoCollapseCardN",
+ hint: "SETTINGS.5eAutoCollapseCardL",
+ scope: "client",
+ config: true,
+ default: false,
+ type: Boolean,
+ onChange: (s) => {
+ ui.chat.render();
+ }
+ });
- /**
- * Option to automatically collapse Item Card descriptions
- */
- game.settings.register("sw5e", "autoCollapseItemCards", {
- name: "SETTINGS.5eAutoCollapseCardN",
- hint: "SETTINGS.5eAutoCollapseCardL",
- scope: "client",
- config: true,
- default: false,
- type: Boolean,
- onChange: s => {
- ui.chat.render();
- }
- });
+ /**
+ * Option to allow GMs to restrict polymorphing to GMs only.
+ */
+ game.settings.register("sw5e", "allowPolymorphing", {
+ name: "SETTINGS.5eAllowPolymorphingN",
+ hint: "SETTINGS.5eAllowPolymorphingL",
+ scope: "world",
+ config: true,
+ default: false,
+ type: Boolean
+ });
- /**
- * Option to allow GMs to restrict polymorphing to GMs only.
- */
- game.settings.register('sw5e', 'allowPolymorphing', {
- name: 'SETTINGS.5eAllowPolymorphingN',
- hint: 'SETTINGS.5eAllowPolymorphingL',
- scope: 'world',
- config: true,
- default: false,
- type: Boolean
- });
-
- /**
- * Remember last-used polymorph settings.
- */
- game.settings.register('sw5e', 'polymorphSettings', {
- scope: 'client',
- default: {
- keepPhysical: false,
- keepMental: false,
- keepSaves: false,
- keepSkills: false,
- mergeSaves: false,
- mergeSkills: false,
- keepClass: false,
- keepFeats: false,
- keepPowers: false,
- keepItems: false,
- keepBio: false,
- keepVision: true,
- 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"
- }
- });
+ /**
+ * Remember last-used polymorph settings.
+ */
+ game.settings.register("sw5e", "polymorphSettings", {
+ scope: "client",
+ default: {
+ keepPhysical: false,
+ keepMental: false,
+ keepSaves: false,
+ keepSkills: false,
+ mergeSaves: false,
+ mergeSkills: false,
+ keepClass: false,
+ keepFeats: false,
+ keepPowers: false,
+ keepItems: false,
+ keepBio: false,
+ keepVision: true,
+ 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"
+ }
+ });
};
diff --git a/module/templates.js b/module/templates.js
index a27bdf71..e810b6da 100644
--- a/module/templates.js
+++ b/module/templates.js
@@ -3,34 +3,33 @@
* Pre-loaded templates are compiled and cached for fast access when rendering
* @return {Promise}
*/
-export const preloadHandlebarsTemplates = async function() {
- return loadTemplates([
+export const preloadHandlebarsTemplates = async function () {
+ return loadTemplates([
+ // Shared Partials
+ "systems/sw5e/templates/actors/parts/active-effects.html",
- // Shared Partials
- "systems/sw5e/templates/actors/parts/active-effects.html",
+ // Actor Sheet Partials
+ "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/oldActor/parts/actor-notes.html",
- // Actor Sheet Partials
- "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/oldActor/parts/actor-notes.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-crew.html",
+ "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.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-force-powerbook.html",
+ "systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html",
+ "systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
+ "systems/sw5e/templates/actors/newActor/parts/swalt-traits.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-crew.html",
- "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.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-force-powerbook.html",
- "systems/sw5e/templates/actors/newActor/parts/swalt-tech-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"
- ]);
+ // 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"
+ ]);
};
diff --git a/module/token.js b/module/token.js
index db811603..873372cc 100644
--- a/module/token.js
+++ b/module/token.js
@@ -3,11 +3,10 @@
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
-
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
- if ( data && (data.attribute === "attributes.hp") ) {
+ if (data && data.attribute === "attributes.hp") {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
@@ -15,19 +14,16 @@ export class TokenDocument5e extends TokenDocument {
}
}
-
/* -------------------------------------------- */
-
/**
* Extend the base Token class to implement additional system-specific logic.
* @extends {Token}
*/
export class Token5e extends Token {
-
/** @inheritdoc */
_drawBar(number, bar, data) {
- if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data);
+ if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data);
return super._drawBar(number, bar, data);
}
@@ -41,7 +37,6 @@ export class Token5e extends Token {
* @private
*/
_drawHPBar(number, bar, data) {
-
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
@@ -58,42 +53,50 @@ export class Token5e extends Token {
// Determine colors to use
const blk = 0x000000;
- const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]);
+ const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
- let h = Math.max((canvas.dimensions.size / 12), 8);
- if ( this.data.height >= 2 ) h *= 1.6;
+ let h = Math.max(canvas.dimensions.size / 12, 8);
+ if (this.data.height >= 2) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
- const bs1 = bs+1;
+ const bs1 = bs + 1;
// Overall bar container
- bar.clear()
+ bar.clear();
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
- bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
+ bar.beginFill(c.tempmax, 1.0)
+ .lineStyle(1, blk, 1.0)
+ .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
- bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
+ bar.beginFill(c.negmax, 1.0)
+ .lineStyle(1, blk, 1.0)
+ .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Health bar
- bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2)
+ bar.beginFill(hpColor, 1.0)
+ .lineStyle(bs, blk, 1.0)
+ .drawRoundedRect(0, 0, valuePct * w, h, 2);
// Temporary hit points
- if ( temp > 0 ) {
- bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
+ if (temp > 0) {
+ bar.beginFill(c.temp, 1.0)
+ .lineStyle(0)
+ .drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
}
// Set position
- let posY = (number === 0) ? (this.h - h) : 0;
+ let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY);
}
}
diff --git a/package-lock.json b/package-lock.json
index 759e9b50..b2fbf9ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1266,9 +1266,9 @@
}
},
"hosted-git-info": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
- "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
},
"image-size": {
"version": "0.5.5",
@@ -3068,9 +3068,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
- "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
+ "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
},
"yargs": {
"version": "7.1.1",
diff --git a/packs/Icons/Force Powers/Defensive Technique.webp b/packs/Icons/Force Powers/Defensive Technique.webp
new file mode 100644
index 00000000..6736a7c0
Binary files /dev/null and b/packs/Icons/Force Powers/Defensive Technique.webp differ
diff --git a/packs/Icons/Force Powers/Kinetite.webp b/packs/Icons/Force Powers/Kinetite.webp
new file mode 100644
index 00000000..c8936b27
Binary files /dev/null and b/packs/Icons/Force Powers/Kinetite.webp differ
diff --git a/packs/Icons/Force Powers/Tapas.webp b/packs/Icons/Force Powers/Tapas.webp
new file mode 100644
index 00000000..14681378
Binary files /dev/null and b/packs/Icons/Force Powers/Tapas.webp differ
diff --git a/packs/Icons/Force Powers/Telepathic Link.webp b/packs/Icons/Force Powers/Telepathic Link.webp
new file mode 100644
index 00000000..4f1383fc
Binary files /dev/null and b/packs/Icons/Force Powers/Telepathic Link.webp differ
diff --git a/packs/Icons/Force Powers/Wakefulness.webp b/packs/Icons/Force Powers/Wakefulness.webp
new file mode 100644
index 00000000..9aeec48a
Binary files /dev/null and b/packs/Icons/Force Powers/Wakefulness.webp differ
diff --git a/packs/Icons/Martial Blasters/Shoulder Cannon.webp b/packs/Icons/Martial Blasters/Shoulder Cannon.webp
new file mode 100644
index 00000000..ec849135
Binary files /dev/null and b/packs/Icons/Martial Blasters/Shoulder Cannon.webp differ
diff --git a/packs/Icons/Simple Vibroweapons/Unarmed Strike.png b/packs/Icons/Simple Vibroweapons/Unarmed Strike.png
new file mode 100644
index 00000000..4b50289b
Binary files /dev/null and b/packs/Icons/Simple Vibroweapons/Unarmed Strike.png differ
diff --git a/packs/Icons/Tech Powers/Aid Droid.webp b/packs/Icons/Tech Powers/Aid Droid.webp
new file mode 100644
index 00000000..a8433e01
Binary files /dev/null and b/packs/Icons/Tech Powers/Aid Droid.webp differ
diff --git a/packs/Icons/Tech Powers/Alter Self.webp b/packs/Icons/Tech Powers/Alter Self.webp
new file mode 100644
index 00000000..a8433e01
Binary files /dev/null and b/packs/Icons/Tech Powers/Alter Self.webp differ
diff --git a/packs/Icons/Tech Powers/Mutate-Augment.webp b/packs/Icons/Tech Powers/Mutate-Augment.webp
new file mode 100644
index 00000000..a8433e01
Binary files /dev/null and b/packs/Icons/Tech Powers/Mutate-Augment.webp differ
diff --git a/packs/Icons/Tech Powers/Tri-Shot.webp b/packs/Icons/Tech Powers/Tri-Shot.webp
new file mode 100644
index 00000000..a8433e01
Binary files /dev/null and b/packs/Icons/Tech Powers/Tri-Shot.webp differ
diff --git a/packs/packs/archetypes.db b/packs/packs/archetypes.db
index b9076c88..f4a4c086 100644
--- a/packs/packs/archetypes.db
+++ b/packs/packs/archetypes.db
@@ -59,7 +59,7 @@
{"_id":"YByrgf4R9lfeVVBQ","name":"Sharpshooter Practice","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Operative","description":{"value":"Sharpshooter Practice \nThose operatives who choose the Sharpshooter Practice are bringers of death. Striking from a safe distance, the Sharpshooter uses precision shooting to control the battlefield and bring targets down quickly.
\nAssume the Position \nBeginning at 3rd level, you don't need advantage on your attack roll to use your Sneak Attack if your target is greater than 30 feet from you and no enemies are within 5 feet of you. In addition, standing up from prone now only costs 5 feet of movement.
\nAdditionally, you gain proficiency with two martial blasters of your choice.
\nPlace Shot \nAlso at 3rd level, you perfect the art of placing distant shots for maximum effectiveness in debilitating and controlling your enemies. When you deal Sneak Attack damage to a creature, you may choose to forgo two of your Sneak Attack dice to make the attack a placed shot.
\nSome of your placed shots require your target to make a saving throw to resist the placed shot's effects. The saving throw DC is calculated as follows:
\nPlaced Shot save DC = 8 + your proficiency bonus + your Dexterity modifier
\nDisarming Shot \nYou attempt to disarm a creature with your attack. The target must succeed on a Strength saving throw or be forced to drop one item of your choice that it's holding. The object lands at its feet.
\nPenetrating Shot \nYou attempt to damage another target with the same attack. Choose a second target within 15 feet of and directly behind your initial target. If the original attack roll would hit the second target, it takes two dice worth of Sneak Attack damage.
\nThe damage is of the same type dealt by the original attack.
\nSuppressive Shot \nYou attempt to pin the target to its location. The target must succeed on a Wisdom saving throw or be frightened of you until the end of its next turn.
\nHead Shot \nAt 9th level, you are at your deadliest when your enemies are unaware of the danger they are in. You have advantage on attack rolls against any creature that hasn't taken a turn in combat yet.
\nAdditionally, any hit you score against a creature that is surprised is a critical hit.
\nDistracting Shot \nStarting at 13th level, you are able to defend your compatriots from afar. When a friendly creature you can see within your weapon's normal range is the target of a ranged attack, or forced to make a saving throw, and the source of the effect is within your weapon's normal range, you can use your reaction to make a ranged weapon attack against the source. On a hit, instead of dealing damage, the target of your attack has disadvantage on the attack roll against your ally, or your ally has advantage on the saving throw to resist the effect.
\nDouble Tap \nAt 17th level, you've learned to capitalize when you have the advantage. When you take the Attack action and make an attack with advantage, you can choose to forgo the advantage. If you do, you can make an additional attack against the target or another creature within 5 feet of it (no action required). Both attacks can benefit from your Sneak Attack damage, instead of only one.
"},"source":"PHB","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Sharpshooter%20Practice.webp","effects":[]}
{"_id":"YwAHQuiEetUQgshY","name":"Triage Technique","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Scout","description":{"value":"Triage Technique \nWith every conflict comes death and destruction. Followers of the Triage Technique excel at keeping their comrades in the fight with quick thinking and fastacting medicine.
\nTriage Training \nTriage Technique: 3rd level
\nYou gain proficiency in the Medicine skill.
\nAdditionally, when you would use your action to make an ability check to stabilize a creature, expend a use of a traumakit, or use a medpac, you can instead use your bonus action.
\nMark of Triage \nTriage Technique: 3rd level
\nYou learn new ways to use your Ranger's Quarry.
\n\nWhile a hostile creature is the target of your Ranger's Quarry, you always know any conditions it is suffering from, and you know at roughly what percentage its current hit points is relative to its maximum. Additionally, if the target is within 60 feet of you. when it is forced to make a Constitution saving throw, you can use your reaction to force it to make the roll with disadvantage. Once you've used this feature, you must complete a short or long rest before you can use it again. \nWhile a friendly creature is the target of your Ranger's Quarry, you have advantage on Wisdom (Medicine) checks made to stabilize it. Additionally, if the target is within 60 feet of you, you can use your bonus action and roll your Ranger's Quarry and either restore hit points equal to the amount rolled, or grant temporary hit points equal to the amount rolled. Once a friendly creature has benefited from this ability, they can not do so again until they complete a short or long rest. \n \nDouble Dose \nTriage Technique: 7th level
\nYour application of medicine does not interfere with your own ability to recover from injuries. When you restore hit points or grant temporary hit points to another creature with a tech power or class feature, you recover the same amount of hit points or gain the same number of temporary hit points.
\nYou can use this feature a number of times equal to your proficiency bonus, as shown in the scout table. You regain all expended uses when you complete a long rest.
\nExperimental Infusion \nTriage Technique: 11th level
\nWhen you target a creature with your Ranger's Quarry, you can grant one of the following additional effects of your choice:
\n\nAdrenaline/Tranquilizer . The creature's movement speed is doubled until the end of its next turn. Alternatively, it gains a level of slowed until the end of its next turn. \nFocus/Dizziness. The creature has either advantage or disadvantage (your choice) on the first ability check, attack roll, or saving throw it makes within the next minute. \nToughen/Weaken. The creature gains temporary hit points equal to your scout level, which last for 1 minute. Alternatively, the creature must make a Constitution saving throw against your tech save DC. On a failure, it takes psychic damage equal to your scout level and it can't regain hit points until the start of your next turn. \n \nYou can use each feature once. You regain any expended uses when you complete a short or long rest.
\nCure-All \nTriage Technique: 15th level
\nYour healing becomes even more potent When you restore hit points to a creature as a bonus action using your Mark of the Healer feature, you can also end one of the following conditions afflicting it: blinded, deafened, diseased, paralyzed, or poisoned.
"},"source":"EC","classCasterType":""},"flags":{"core":{"sourceId":"Item.p9sVycmJvwVtDzlD"}},"img":"systems/sw5e/packs/Icons/Archetypes/Triage%20Technique.webp","effects":[]}
{"_id":"ZDNCB88TzeMFGY6i","name":"Deadeye Technique","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Scout","description":{"value":"Deadeye Technique \nSome scouts becomes legends written in blaster burns. Followers of the Deadeye Technique the art of the blaster shot and utilize their incredible focus to make shots that most would deem impossible. When everything depends on one shot, you want a Deadeye pulling the trigger.
\nDue to their uncanny focus, Deadeyes can make shots that other marksmen would never dare to attempt. Deadeyes know how to make the most of ranged weapons and can use them to greater effect than other Scouts.
\nFocused Superiority \nWhen you choose this technique at 3rd level, you learn maneuvers that are fueled by special dice called superiority dice.
\nManeuvers \nYou know two maneuvers of your choice, which are detailed under \"Maneuvers\" below, and you earn more at higher levels, as shown in the Maneuvers Known column of the Deadeye Technique Focused Superiority table. Many maneuvers enhance an attack in some way. You can use only one maneuver per attack, and you may only use each maneuver once per turn.
\nEach time you learn new maneuvers, you can also replace one maneuver you know with a different one.
\nSuperiority Dice \nYou have two superiority dice, which are d6s, and you earn more at higher levels, as shown in the Superiority Dice column of the Deadeye Technique Focused Superiority table. A superiority die is expended when you use it. You regain all of your expended superiority dice when you finish a short or long rest.
\nSaving Throws \nSome of your maneuvers require your target to make a saving throw to resist the maneuver's effects. The saving throw DC is calculated as follows:
\nManeuver save DC = 8 + your proficiency bonus + your Dexterity modifier
\n\n
\n\n\n\nDeadeye Technique Focused Superiority \n \n\nLevel \nFocused Superiority \nSuperiority Dice \nManeuvers Known \n \n \n\n\n3rd \nd4 \n2 \n2 \n \n\n4th \nd4 \n2 \n2 \n \n\n5th \nd6 \n2 \n2 \n \n\n6th \nd6 \n2 \n2 \n \n\n7th \nd6 \n3 \n3 \n \n\n8th \nd6 \n3 \n3 \n \n\n9th \nd8 \n3 \n3 \n \n\n10th \nd8 \n3 \n3 \n \n\n11th \nd8 \n4 \n4 \n \n\n12th \nd8 \n4 \n4 \n \n\n13th \nd10 \n4 \n4 \n \n\n14th \nd10 \n4 \n4 \n \n\n15th \nd10 \n5 \n5 \n \n\n16th \nd10 \n5 \n5 \n \n\n17th \nd12 \n5 \n5 \n \n\n18th \nd12 \n5 \n5 \n \n\n19th \nd12 \n5 \n5 \n \n\n20th \nd12 \n5 \n5 \n \n \n
\n \n
\nManeuvers \nThe maneuvers are presented in alphabetical order.
\nCrippling Shot \nWhen you hit a creature with a ranged weapon attack, you can expend a superiority die to cripple its movement. Add the number rolled to the damage of the ranged weapon attack. The creature must succeed on a Constitution saving throw or have its movement speed halved. At the end of each of its turns, the target can make a Constitution saving throw to end the effect.
\nDaring Escape \nYou can expend one superiority die to take the Disengage action as a bonus action until the end of your turn. Until the end of this turn, you have advantage on all Strength (Athletics) checks.
\nCovering Fire \nWhen you hit a creature with a ranged weapon attack, you can expend one superiority die to maneuver one of your comrades into a more advantageous position. You add the superiority die to the attack's damage roll, and you choose a friendly creature who can see or hear you.
\nThat creature can use its reaction to move up to half its speed without provoking opportunity attacks from the target of your attack.
\nDisarming Shot \nWhen you hit a creature with a ranged weapon attack, you can expend one superiority die to attempt to disarm the target, forcing it to drop one item of your choice that it's holding. Add the superiority die to the attack's damage roll, and the target must make a Strength saving throw. On a failed save, it drops the object you choose. The object lands at its feet.
\nDistracting Shot \nWhen you hit a creature with a ranged weapon attack, you can expend one superiority die to distract the creature, giving your allies an opening. You add the superiority die to the attack's damage roll. The next attack roll against the target by an attacker other than you has advantage if the attack is made before the start of your next turn.
\nExploit Weakness \nWhen you hit a creature with a weapon attack, you can expend a superiority die and deal additional damage equal to the number rolled. This damage cannot be reduced in any way.
\nPenetrating Shot \nWhen you hit a creature with a ranged weapon attack, you can expend one superiority die to attempt to damage another creature with the same attack. Choose up to two creatures within 15 feet of and directly behind your initial target. If the original attack roll would hit the second creature(s), it takes damage equal to the number you roll on your superiority die.
\nThe damage is of the same type dealt by the original attack.
\nPrecision Attack \nWhen you make a weapon attack roll against a creature, you can expend one superiority die to add it to the roll. You can use this maneuver before or after making the attack roll, but before any effects of the attack are applied.
\nReturn Fire \nWhen a creature misses you with a ranged attack, you can use your reaction and expend one superiority die to make a ranged weapon attack against the creature. If you hit, you add the superiority die to the attack's damage roll.
\nMark of the Deadeye \nAlso at 3rd level, the range of your Ranger's Quarry feature doubles. Additionally, when making ranged weapon attacks against the target of your Ranger's Quarry, the normal and long range of your ranged weapons double.
\nCover to Cover \nBeginning at 7th level, attack rolls made against you on your turn are made with disadvantage.
\nDeadeye Technique Focused Superiority \nShoot First \nStarting at 11th level, you have learned that the person who shoots first is often the one who walks out alive. When you make a ranged weapon attack against a creature that has not yet acted during your first turn in combat and you have advantage on the roll, you can reroll one of the dice once.
\nAdditionally, on a hit, you deal an extra 1d6 damage of the same type as the weapon.
\nOverwatch \nAt 15th level, you have become a master at protecting your allies from afar. When a creature attempts to make an opportunity attack against an allied creature, or forces your ally to make a saving throw, you can use your reaction to make an attack roll against the enemy creature.
\nIf your attack hits, either impose disadvantage on the enemy creature's opportunity attack roll or grant advantage to any allies making the saving throw.
"},"source":"PHB","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Techcaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Deadeye%20Technique.webp","effects":[]}
-{"_id":"avFn1m9oUpDgKAAF","name":"Astrotech Engineering","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Engineer","description":{"value":"Astrotech Engineering \nThose engineers who choose the Astrotech Engineering discipline focus on crafting and upgrading their droid companions.
\nBonus Proficiencies \nWhen you choose this discipline at 3rd level, you gain proficiency in astrotech's tools. Additionally, when you engage in crafting with astrotech's tools, the rate at which you craft doubles.
\nDroid Companion \nAlso at 3rd level, you learn to employ all the knowledge you've accumulated to create and customize your own personalized droid companion.
\nChoose your droid, which is detailed at the end of this discipline. Over the course of 8 hours, which can be done during a long rest, you can expend 500 cr worth of materials to finally finish your droid.
\nIf your droid is irreparably destroyed, or you want to interface with a different droid, you can spend an additional 250 credits and 1 hour to change the target of this feature. You may only have one droid companion at a time.
\nYour droid gains a variety of benefits while it is interfaced with you:
\n\nThe droid obeys your commands as best it can. It acts on your turn, and you determine its actions, decisions, attitudes, and so on. If you are incapacitated or absent, your droid acts on its own. \nYour droid's level equals your engineer level, and for each engineer level you gain after 3rd, your droid companion gains an additional Hit Die and increases its hit points accordingly. \nYour droid has the proficiency bonus of a player character of the same level. \nWhenever you gain the Ability Score Improvement feature in this class, your droid's abilities also improve. Your droid can increase one ability score of your choice by 2, or it can increase two ability scores of your choice by 1. As normal, your droid can't increase an ability score above 20 using this feature unless its description specifies otherwise. \nYour droid can not wear armor, but you can have the armor professionally integrated into its chassis. Over the course of a long rest, you can expend materials equal to half the cost of the armor in order to integrate it into your droid. Your droid must be proficient in armor in order to have it integrated. \nYour droid is a valid target of the tracker droid interface tech power. \n \nAdditionally, you can modify your droid. Your droid companion has 4 modification slots, and it gains more at higher levels, as shown in the Modification Slots column of the engineer class table. For each modification installed, your tech point maximum is reduced by 1. Over the course of a long rest, you can replace or remove a number of modifications up to your Intelligence modifier (minimum of one).
\nPotent Integration \nLastly at 3rd level, when your droid makes an attack roll, you can use your reaction to expend one use of your Potent Aptitude to give it a boost. Roll the die and add it to both the attack and damage rolls, if the attack hits.
\nCoordinated Attack \nBeginning at 6th level, when you take the Attack action, if your companion can see you, it can use its reaction to make a weapon attack against the same creature.
\nDroid Defense \nAt 14th level, while your droid can see you, it has advantage on all saving throws.
\nSuperior Droid Defense \nStarting at 18th level, whenever an attacker that your droid can see hits it with an attack, it can use its reaction to halve the attack's damage against it.
\nGenerating Your Droid \nChoosing your droid companion is an integral part of being an Astrotech Engineer. The class of droid you choose determines their features. Class II, III, and IV droids are all appropriate options, with their statistics listed below.
\nOnce you've selected your type of droid class, you assign your droid's ability scores using standard array (16, 14, 14, 12, 10, 8) as you see fit.
\nDroid Features \nAll droids share the following features.
\nResistances and Vulnerabilities \n\nDroid Resistances: Your droid is resistant to necrotic, poison, and psychic damage, and immune to poison and disease. \nDroid Vulnerabilities: Your droid is vulnerable to ion damage. Additionally, your droid has disadvantage on saving throws against effects that would deal ion or lightning damage. \n \nTraits \n\nCreature Type: Droid \nArmor Integration: Your droid can not wear armor, but you can have the armor professionally integrated into its chassis. Over the course of a long rest, you can expend materials equal to half the cost of the armor in order to have it integrated. This work must be done by someone proficient with astrotech's tools. Your droid must be proficient in armor in order to have it integrated. \n \nClass I Droid \nClass I droids are programmed for the mathematical, medical, or physical sciences. Subcategories of the first degree are medical droids, biological science droids, physical science droids, and mathematics droids.
\nAs a class I droid, your droid companion has the following features.
\nHit Points \n\nHit Dice: 1d8 per class I droid level \nHit Points at 1st Level: 8 + your droid's Constitution modifier \nHit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class I droid level after 1st \n \nProficiencies \n\nArmor: Light armor plating \nWeapons: Simple blasters, simple vibroweapons \nTools: None \n \n\nLanguages: Class I droids can speak, read, and write Galactic Basic and one language of your choice. They can understand spoken and written Binary, but can not speak it \nSaving Throws: Wisdom, Intelligence \nSkills: Two Intelligence, Wisdom, or Charisma skills of your choice \n \nFeatures \n\nSize: Medium \nSpeed: 25 ft. \n \nClass II Droid \nClass II droids are programmed for engineering and other technical sciences. They differ from class I droids because they apply the science to real-life situations. Class II droids are rarely equipped with Basic vocabulators, instead communicating through Binary. There are five subcategories of class II droids. Astromech, exploration, environmental, engineering, and maintenance droids are all class II droids.
\nAs a class II droid, your droid companion has the following features.
\nHit Points \n\nHit Dice: 1d6 per class II droid level \nHit Points at 1st Level: 6 + your droid's Constitution modifier \nHit Points at Higher Levels: 1d6 (or 4) + your droid's Constitution modifier per class II droid level after 1st \n \nProficiencies \n\nArmor: Light armor, medium armor \nWeapons: Simple blasters and simple vibroweapons with the light property \nTools: Your choice of demolition's kit, security kit, or slicer's kit \n \n\nLanguages: Class II droids can speak, read, and write Binary. They can understand spoken and written Galactic Basic and one language of your choice, but can not speak it \nSaving Throws: Dexterity, Intelligence \nSkills: Two of your choice \n \nFeatures \n\nSize: Small \nSpeed: 25 ft. \n \nClass III Droid \nClass III droids are programmed to interact with humans. They are said to be the most advanced droids ever invented. Protocol, servant, tutor, and child care droids are all class III droids.
\nAs a class III droid, your droid companion has the following features.
\nHit Points \n\nHit Dice: 1d8 per class III droid level \nHit Points at 1st Level: 8 + your droid's Constitution modifier \nHit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class III droid level after 1st \n \nProficiencies \n\nArmor: Light armor \nWeapons: Simple blasters, simple vibroweapons \nTools: None \n \n\nLanguages: Class III droids can speak and understand all registered languages \nSaving Throws: Wisdom, Charisma \nSkills: None \n \nFeatures \n\nSize: Medium \nSpeed: 25 ft. \n \nClass IV Droid \nClass IV droids are programmed for military and security purposes. Such droids tend to perform tasks of violence or combat might be expected. Almost all class IV droids carry weapons. Armed combat droids are among the first droids ever created. Security, gladiator, battle, and assassin droids are all class IV droids.
\nAs a class IV droid, your droid companion has the following features.
\nHit Points \n\nHit Dice: 1d8 per class IV droid level \nHit Points at 1st Level: 8 + your droid's Constitution modifier \nHit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class IV droid level after 1st \n \nProficiencies \n\nArmor: All armor \nWeapons: All blasters, All vibroweapons \nTools: None \n \n\nLanguages: Class IV droids can speak, read, and write Galactic Basic and one language of your choice. They can understand spoken and written Binary, but can not speak it \nSaving Throws: Strength, Dexterity \nSkills: None \n \nFeatures \n\nSize: Medium \nSpeed: 30 ft. \n \nClass V Droid \nClass V droids are programmed for menial and low-skill tasks. Such droids tend to perform basic tasks such as construction, lifting, maintenance, mining, sanitation, and transportation.
\nAs a class V droid, your droid companion has the following features.
\nHit Points \n\nHit Dice: 1d8 per class V droid level \nHit Points at 1st Level: 8 + your droid's Constitution modifier \nHit Points at Higher Levels: 1d8 (or 5) + your droid's Constitution modifier per class V droid level after 1st \n \nProficiencies \n\nArmor: Light armor, medium armor \nWeapons: All vibroweapons, simple blasters \nTools: One set of artisan's tools \n \n\nLanguages: Class V droids can speak, read, and write Binary. They can understand spoken and written Galactic Basic and one language of your choice, but can not speak it \nSaving Throws: Strength, Constitution \nSkills: Athletics \n \nFeatures \n\nSize: Medium \nSpeed: 30 ft. \n \nAstrotech Modifications \nIf a modification has prerequisites, you must meet them to install it. You can install the modification at the same time that you meet its prerequisites.
\nAdvanced Power Core \nPrerequisite: 7th level, d10 Hit Die You greatly improve the power core of your droid. Its Hit Die becomes a d12.
\nAlarm Protocol \nYou install an alarm module in your droid, granting the following benefits:
\n\nYour droid grants a +5 bonus to initiative to creatures within 5 feet of it. \nYou and your droid can't be surprised while your droid is conscious. \n \nAnalysis Protocol \nPrerequisite: 7th level, Class I Droid Your droid can analyze a target, develop a plan on how to best overcome any potential obstacle, and execute that plan with ruthless efficiency. As a bonus action on your droid's turn, your droid can analyze a target it can see within 60 feet of it. For the next minute, or until it analyzes another target, it gains the following benefits:
\n\nWhen it analyzes a hostile creature, its attack and damage rolls made with weapons with the finesse property or blaster weapons against that target may use its Intelligence modifier instead of Strength or Dexterity. \nWhen it analyzes a friendly creature, the target can end your droid's Analysis Protocol on them (no action required) to add half your droid's Intelligence modifier (rounded down, minimum of +1) to one attack roll, ability check, or saving throw. Once a friendly creature has benefited from this ability, they can not do so again until they complete a short or long rest. \n \nArm Cannons \nYou install dual arm cannons in your droid. The arm cannons have 2 charges. As an action, your droid can use charges to cast the overload tech power, using 1 charge per level. The saving throw is made against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier).
\nYou can choose this modification multiple times. Each time you do so, the arm cannons gain another charge, to a maximum of 4. The arm cannons regain all charges after a long rest.
\nBack-Up Protocol \nPrerequisite: 7th level You install an emergency protocol in your droid, prompting a quick reboot after critical damage is taken. If your droid would be reduced to 0 hit points, it instead is reduced to 1.
\nOnce your droid uses this feature, it must finish a short or long rest before it can use it again.
\nCelerity Augment \nYou augment your droid to move a little faster. Your droid's speed increases by 5 feet.
\nYou can choose this modification twice.
\nCharisma Chip \nPrerequisite: Class III Droid You install a charisma chip in your droid. When an ally your droid can see makes an ability check, attack roll, or saving throw, your droid can use its reaction to give them advantage on the roll. It can do so before or after they roll the d20, but before the GM says the roll succeeds or fails. Once your droid uses this ability, it can't use it again until it finishes a short or long rest.
\nDurability Module \nYou enhance your droid's durability, granting the following benefits:
\n\nWhen your droid rolls a Hit Die to regain hit points, the minimum number of hit points your droid can regain from the roll equals twice your droid's Constitution modifier (minimum of 2). \nYour droid's hit point maximum increases by an amount equal to twice its level when you install this protocol. Whenever your droid gains a level thereafter, its hit point maximum increases by an additional 2 hit points. \n \nEmergency Mode \nPrerequisite: 15th level Prerequisite: Back-Up Protocol You modify your droid's back-up protocol. When your droid's back-up protocol is initiated, it can immediately use its reaction to make one attack roll against a target within range. If the target is the source of the damage that reduced your droid to 0, the attack roll has advantage.
\nEnergy Shield \nYou install an energy shield in your droid. The energy shield has 1 charge. As an action, your droid can use 1 charge to cast the @Compendium[sw5e.techpowers.c7vvcY0lZDii7SOI]{Energy Shield} tech power.
\nYou can choose this modification multiple times. Each time you do so, the energy shield gains another charge, to a maximum of 3. The energy shield regains all expended charges after a long rest.
\nExpertise Protocol \nPrerequisite: 5th level You install a protocol in your droid that grants it expertise in a tool or skill. Choose a tool or skill that your droid is proficient in. Your droid gains expertise in it.
\nFalse Combustion \nPrerequisite: Class II Droid You install a panic protocol in your droid. As a reaction in response to taking damage, your droid can feign an explosion. For 1 minute, your droid appears to be destroyed to all outward inspection. A creature can use its action to inspect the droid and make an Intelligence (Investigation) check against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier). If it succeeds, it becomes aware that your droid is still functioning.
\nFighting Style Protocol \nYour droid adopts a particular style of fighting as its specialty. Choose one of the Fighting Style options, detailed in chapter 6. Your droid can't take a Fighting Style option more than once, even if it later gets to choose again.
\nFlamethrower \nYou install a flamethrower in your droid. The flamethrower has 1 charge. As an action, your droid can cast the @Compendium[sw5e.techpowers.HoshRCTHW8vntDCg]{Jet of Flame} tech power or use 1 charge to cast the flame sweep tech power at 1st level. The saving throw is made against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier).
\nYou can choose this modification multiple times. Each time you do so, the flamethrower gains another charge, to a maximum of 3. If the flamethrower has multiple charges, you can use multiple charges to cast @Compendium[sw5e.techpowers.6aZ0FG6HwrUO28WF]{Flame Sweep} at a higher level, 1 point per charge. The flamethrower regains all expended charges after a long rest.
\nFour-Armed Combatant \nPrerequisite: Class IV Droid You install two additional arms to improve your droid's combat capabilities, granting it four arms which it can use independently of one another. Your droid can only gain the benefit of items held by two of its arms at any given time, and once per round your droid can switch which arms it is benefiting from (no action required).
\nWhile your droid has at least 3 arms free, it has a climbing speed equal to its walking speed.
\nHeavy Plating \nPrerequisite: Medium Armor proficiency Your droid gains proficiency in heavy armor. If your droid is already proficient in heavy armor, instead kinetic and energy damage that your droid takes from unenhanced weapons is reduced by an amount equal to its proficiency bonus.
\nLight Plating \nYour droid gains proficiency in light armor. If your droid is already proficient in light armor, instead your droid's speed increases by 5 feet while light armor is integrated.
\nMartial Protocol \nPrerequisite: 7th level, Class IV Droid Your droid has martial training that allows it to perform special combat maneuvers. It gains the following benefits:
\n\nIt learns two maneuvers of your choice from among those available to the fighter class. If a maneuver it uses requires its target to make a saving throw to resist the maneuver's effects, the saving throw DC equals 8 + your droid's proficiency bonus + your droid's Strength or Dexterity modifier (your choice). \nYour droid has two superiority dice, which are d4s. These dice are used to fuel its maneuvers. A superiority die is expended when your droid uses it. It regain all of its expended superiority dice when you finish a short or long rest. \n \nMedium Plating \nPrerequisite: Light Armor proficiency Your droid gains proficiency in medium armor. If your droid is already proficient in medium armor, the maximum Dexterity bonus your droid can add to AC increases to 3 from 2 while medium armor is integrated.
\nMemory Protocol \nPrerequisite: Class I Droid Your droid can recall anything it has read in the past month that it understood. This includes but is not limited to books, maps, signs, and lists.
\nObservant Protocol \nPrerequisite: 7th level Prerequisite: Alarm Protocol You modify the alarm module in your droid, granting the following benefits:
\n\nIf your droid can see a creature's mouth while it is speaking a language it understands, your droid can interpret what it's saying by reading its lips. \nYour droid is considered to have advantage when determining its passive Wisdom (Perception) and passive Intelligence (Investigation) scores. \n \nPerformance Protocol \nPrerequisite: 7th level, Class III Droid You modify your droid's charisma chip, granting the following benefits:
\n\nYour droid has advantage on Charisma (Performance) checks. \nYour droid can also use its bonus action to motivate an ally within 30 feet of it. Until the start of your droid's next turn, the ally can add the droid's Charisma modifier (minimum of +1) to the first attack roll, ability check, or saving throw they make. Your droid can use this feature a number of time equal to its Charisma modifier, and it regains all expended uses after it completes a long rest. \n \nPowerful Droid \nPrerequisite: Class V Droid Your droid count as one size larger when determining your carrying capacity and the weight you can push, drag, or lift.
\nPowerful Grip \nPrerequisite: 7th level, Class V Droid When your droid hits a creature with a melee weapon attack on its turn and has a free hand, it can use a bonus action to attempt to grapple the target. If it does so, and the grapple succeeds, your droid can make one additional attack against the target (no action required).
\nPremium Power Core \nYou improve the power core of your droid. Its Hit Die becomes a d8.
\nProficiency Protocol \nYou install a protocol in your droid that grants it proficiency in a tool or skill. Your droid gains proficiency in a tool or skill of your choice.
\nPrototype Power Core \nPrerequisite: d8 Hit Die You further improve the power core of your droid. Its Hit Die becomes a d10.
\nRepulsor Coil \nPrerequisite: 7th level, Class II Droid You install repulsor coils in your droid's legs. Your droid gains a flying speed equal to its walking speed.
\nSensor Augmentation \nYou augment your droid with an advanced sensor, granting the following benefits:
\n\nYour droid has advantage on Wisdom (Perception) and Intelligence (Investigation) checks made to detect the presence of secret doors. \nYour droid has advantage on saving throws made to avoid or resist traps. \nYour droid has resistance to the damage dealt by traps. \nYour droid can search for traps while traveling at a normal pace, instead of only at a slow pace. \n \nStun Ray \nYou install a stun ray in your droid. The stun ray has 1 charge. As an action, your droid can use 1 charge to cast the @Compendium[sw5e.techpowers.qHu258wCccbyajwo]{Hold Droid} or @Compendium[sw5e.techpowers.zXCnz8vBWC4fhvfw]{Paralyze Humanoid} tech power. The saving throw is made against your droid's tech save DC (8 + your droid's proficiency bonus + your droid's Intelligence modifier).
\nYou can choose this modification multiple times. Each time you do so, the stun ray gains another charge, to a maximum of 3. The stun ray regains all expended charges after a long rest.
\nTechcasting Protocol \nYour droid learns two at-will tech powers, and one 1st-level tech power, which it casts at its lowest level. Once your droid casts it, your droid must finish a long rest before it can cast it again. Intelligence is your droid's techcasting ability for these powers. It does not require use of a wristpad for these powers.
\nToughness Module \nPrerequisite: 11th level Prerequisite: Durability Module You modify the durability module in your droid, granting the following benefit:
\n\nYour droid becomes proficient in Constitution saving throws. If it is already proficient, it becomes proficient in another saving throw of your choice. \nWhenever your droid takes the Dodge action in combat, it can spend one Hit Die to heal itself. Roll the die, add its Constitution modifier, and it regains a number of hit points equal to the total (minimum of one). \n "},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Techcaster"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.traits.toolProf.value","value":"astro","mode":"+","targetSpecific":false,"id":1,"itemId":"avFn1m9oUpDgKAAF","active":false,"_targets":[]}]}},"img":"systems/sw5e/packs/Icons/Archetypes/Astrotech%20Engineering.webp","effects":[]}
+{"_id":"avFn1m9oUpDgKAAF","name":"Astrotech Engineering","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Engineer","description":{"value":"Astrotech Engineering \nThose engineers who choose the Astrotech Engineering discipline focus on utilizing their vast knowledge of droid technology to confuse, confound, and destroy enemy tech, droids and constructs.
\nBonus Proficiencies \nAstrotech Engineering: 3rd level
\nYou gain proficiency in astrotech's implements. Additionally, when you engage in crafting with astrotech's implements, the rate at which you craft doubles.
\nElectronic Warfare Platform \nAstrotech Engineering: 3rd, 9th, and 17th level
\nYou learn to modify your astrotech's implements into a mobile electronic warfare platform. Over the course of a long rest, you can modify your astrotech's implements. You must have astrotech's implements in order to perform this modification.
\nYour electronic warfare platform is enhanced, requires attunement, can only be used by you, and counts as a tech focus for your tech powers while you are attuned to it. Your electronic warfare platform has 4 modification slots, and it gains more at higher levels, as shown in the Modification Slots column of the engineer table. For each modification installed in excess of your proficiency bonus, your tech point maximum is reduced by 1. Over the course of a long rest, you can install, replace, or remove a number of modifications up to your Intelligence modifier (minimum of one).
\nSome modification effects require saving throws. When you use such an effect from this class, the DC equals your tech save DC.
\nYour electronic warfare platform comes equipped with electromagnetic projection and sensing systems, enabling three fields: ionizing, jamming, and targeting. As a bonus action, you can activate a field, which lasts for 1 minute, causing an effect determined by the field. Only one field can be active at a time, and you can end your field at any time on your turn, no action required. The range of your barriers increases to 15 feet at 9th level and 30 feet at 17th level. You can use these fields a combined number of times equal to your proficiency bonus, as shown in the engineer table. You regain all expended uses when you complete a long rest.
\nIonizing Field \nWhenever a droid or construct starts its turn within 5 feet of you, it gains 4 slowed levels and takes ion damage equal to half your engineer level (rounded down).
\nJamming Field \nWhenever you or a friendly creature within 10 feet of you are forced to make a saving throw by a tech power, the creature gains a bonus to the saving throw equal to your Intelligence modifier (minimum of +1).
\nTargeting Field \nYou and friendly creatures within 5 feet of you gain a bonus to the first ranged weapon damage rolls you make each round equal to your Intelligence modifier (minimum of+1).
\nPotent Electromagnetism \nAstrotech Engineering: 3rd level
\nOnce per round, when you deal damage to a droid or construct, you can expend one use of your Potent Aptitude to deal an extra 2d6 damage to that target. The damage is the same type as the source of the damage.
\nThe damage increases when you reach certain levels in this class, increasing to 3d6 at 5th level, 5d6 at 10th level, and 8d6 at 15th level.
\nDirect Controller \nAstrotech Engineering: 6th level
\nYour mastery of droids improves your ability to manipulate them. When you cast a power that could affect only droids or constructs, and you only affect droids with the power, you can choose to treat the power as if cast at your Max Power Level.
\nYou can use this feature a number of times equal to your proficiency bonus, as shown in the engineer table. You regain all expended uses when you complete a long rest.
\nSystems Hijack \nAstrotech Engineering: 14th level
\nAll droids, constructs, and creatures wearing or holding a techcasting focus within 120 feet are viable targets for any tech powers which you cast with a range of touch.
\nElectromagnetic Burst \nAstrotech Engineering: 18th level
\nAs an action, you can end your field in an electromagnetic burst of power with an effect determined by the field you were generating.
\nOnce you've used this feature, you must complete a long rest before you can use it again.
\nIonizing Burst \nChoose up to 10 creatures of your choice that are within 60 feet. Each must make a Constitution saving throw. On a failed save, a target takes 14d6 ion damage and is stunned. On a success, it takes half damage and isn't stunned.
\nJamming Burst \nChoose up to 10 creatures of your choice that are within 60 feet. Once in the next minute, when a chosen creature fails a saving throw caused by a tech power or is hit by an attack power, the creature can choose to succeed on the saving throw or have the attack miss instead.
\nTargeting Burst \nChoose up to 10 creatures of your choice that are within 60 feet. Each creature must succeed on a Constitution saving throw against your tech save DC or be blinded for 1d4+1 turns.
\nAstrotech Modifications \nIf a modification has prerequisites, you must meet them to install it. You can install the modification at the same time that you meet its prerequisites.
\nAI Amplifier \nPrerequisite: 5th level
\nYou gain a +1 bonus to melee tech attack rolls. This bonus increases to +2 at 9th level and +3 at 13th level.
\nAI Rangefinder \nPrerequisite: 5th level
\nYou gain a +1 bonus to ranged tech attack rolls. This bonus increases to +2 at 9th level and +3 at 13th level.
\nAdvanced Hardening \nPrerequisite: 15th level, Hardening
\nYou have resistance to ion damage.
\nAdvanced Grounding System \nPrerequisite: 15th level, Prototype Grounding System
\nYou have resistance to lightning damage.
\nAdvanced Jamming Phased Array \nPrerequisite: 15th level, Prototype Jamming Phased Array
\nWhile your jamming field is active, any hostile creature within the field must make a Constitution saving throw at the end of each of its turns to maintain concentration on the power.
\nAdvanced Ionizing Phased Array \nPrerequisite: 15th level, Prototype Pulsating Phased Array
\nWhile your ionizing field is active, your tech powers and class features ignore resistance to ion damage, and immunity to ion damage is instead treated as resistance from any creature within range of your field.
\nAdditionally, when you use your Ionizing Phased Array feature, you create a fourth blast.
\nAdvanced Targeting Phased Array \nPrerequisite: 15th level, Prototype Targeting Phased Array
\nWhile your targeting field is active, as an action, you can allow a number of allies within the field up to your Intelligence modifier (minimum of one) to use their reaction to make a ranged weapon attack against a single target you can see.
\nOnce you've used this feature, you must complete a short or long rest before you can use it again.
\nDroid Restraints \nWhen you would install a restraining bolt, you can do so in half the time. Additionally, when determining the save DC of a restraining bolt you control, you can use your tech save DC, if it would be higher than the item's DC.
\nFlashlight Attachment \nYou affix a targeted light to your electronic warfare platform. As a bonus action, you can toggle the light on or off. While on, your electronic warfare platform sheds bright light in a 60-foot cone.
\nFrailcasting Controller \nPrerequisite: 5th level
\nYou gain a +1 bonus to the tech save DC of powers you cast that require a Strength or Constitution saving throw. This bonus increases to +2 at 9th level and +3 at 13th level.
\nGrounding System \nYou have advantage on saving throws against lightning damage.
\nHacked Communications \nAs a bonus action, you may choose any number of creatures that you can see within 60 feet of you that have commlinks, headcomms, or other such communications devices. Each creature must succeed on a Constitution saving throw or take sonic damage equal to your Intelligence modifier (minimum of one). Additionally, on a failed save, their communication devices are disabled until rebooted.
\nOnce you've used this feature, you must complete a short or long rest before you can use it again.
\nHardening \nYou have advantage on saving throws against ion damage.
\nIntelligence Core Override \nPrerequisite: 9th level
\nYou can cast the override interface tech power at 5th level without spending tech points.
\nOnce you've used this feature, you must complete a long rest before you can use it again.
\nIonizing Phased Array \nPrerequisite: 5th level
\nWhile your ionizing field is active, as an action, you can send forth blasts of directed ionic energy. Make two ranged tech attacks against targets within the field. These attacks can target the same creature or different ones. Make separate attack rolls for each blast. The attack deals 1d6 ion damage on a hit.
\nJamming Phased Array \nWhile your jamming field is active, as an action, you can choose a number of creatures concentrating on a power equal to your Intelligence modifier (a minimum of one) within your field, and force them to make a Concentration saving throw. If you cause at least one creature to lose concentration on a power using this feature, you can use your reaction to make all creatures that lost concentration take ion damage equal to your Intelligence modifier.
\nMiniaturized Repulsor Coils \nYour electronic warfare platform can store 20 lb., not exceeding 1 cubic foot, which does not count towards your encumbrance.
\nPrototype Transmitter \nPrerequisite: 7th level
\nYou further tweak your transmitter design, extending the area of your fields. When you activate a field you may choose to double the radius of your field.
\nOnce you've used this feature, you must complete a long rest before you can use it again.
\nPrototype Grounding System \nPrerequisite: 7th level, Grounding System
\nYou have immunity to the shocked condition.
\nPrototype Jamming Phased Array \nPrerequisite: 7th level, Jamming Phased Array
\nWhile your jamming field is active, when a creature within your field attempts to cast a tech power, you can use your reaction to cast the tech override power at 3rd level without spending tech points. When you cast this power using this feature and you make the techcasting ability check as a part of this casting, you add your proficiency bonus to the check.
\nOnce you've used this feature, you must complete a short or long rest before you can use it again.
\nPrototype Ionizing Phased Array \nPrerequisite: 7th level, Ionizing Phased Array
\nWhile your ionizing field is active, when you cast a tech power or use a class feature that affects other creatures within the radius of your field, you can choose a number of them equal to 1 + the power's level. The chosen creatures automatically succeed on their saving throws against the power, and they take no damage if they would normally take half damage on a successful save.
\nAdditionally, when you use your Ionizing Phased Array feature, you create a third blast.
\nPrototype Targeting Phased Array \nPrerequisite: 7th level, Target Marking Phased Array
\nWhile your targeting field is active, as an action, you can cast the scramble interface power at 3rd level without spending tech points.
\nOnce you've used this feature, you must complete a short or long rest before you can use it again.
\nRedundant Circuits \nPrerequisite: 15th level
\nYou can have two fields active at a time.
\nRendcasting Controller \nPrerequisite: 5th level
\nYou gain a +1 bonus to the tech save DC of powers you cast that requires a Dexterity or Intelligence saving throw. This bonus increases to +2 at 9th level and +3 at 13th level.
\nSensor Scramblers \nPrerequisite: 5th level
\nAll creatures within 10 feet of you become undetectable to electronic sensors and cameras. Anything these creatures are wearing or carrying is also undetectable, so long as it's on the creature's person. The creatures are still visible to regular vision.
\nOnce you've used this feature, you must complete a long rest before you can use it again.
\nStealth Field Generator \nPrerequisite: 7th level
\nYou integrate a portable, personal cloaking device into your electronic warfare platform. Activating or deactivating the generator requires a bonus action and, while active, you have advantage on Dexterity (Stealth) ability checks that rely on sight. The generator lasts for 1 minute. This effect ends early if you make an attack or cast a force or tech power.
\nOnce the generator has been activated, it can't be activated again until you finish a short or long rest.
\nTargeting Phased Array \nPrerequisite: 5th level
\nWhile your targeting field is active, as an action, you can choose a number of creatures equal to your Intelligence modifier you can see within 60 feet (a minimum of one creature), and force them to make a Dexterity saving throw. On a failed save, all ranged weapon attacks made by allies within your field against the target have advantage until the beginning of your next turn.
\nTruelight Attachment \nPrerequisite: 11th level, Flashlight Attachment
\nYou modify your electronic warfare platform with a toggle allowing you to briefly gain enhanced sight. As a bonus action, you can activate the truesight feature of your electronic warfare platform. When toggled on, for the next minute your electronic warfare platform now automatically dispel illusions and can detect invisibility, as with truesight.
\nOnce you've used this feature, you must complete a short or long rest before you can use it again.
\nWithercasting Controller \nPrerequisite: 5th level
\nYou gain a +1 bonus to the tech save DC of powers you cast that requires a Wisdom or Charisma saving throw. This bonus increases to +2 at 9th level and +3 at 13th level.
\nX-Ray Targeting \nPrerequisite: 5th level
\nYou tweak your sensors to find weak points in thick armor. Your weapon attacks and tech powers deal double damage against constructs.
"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Techcaster"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.traits.toolProf.value","value":"astro","mode":"+","targetSpecific":false,"id":1,"itemId":"avFn1m9oUpDgKAAF","active":false,"_targets":[]}]}},"img":"systems/sw5e/packs/Icons/Archetypes/Astrotech%20Engineering.webp","effects":[]}
{"_id":"bBMsNrnCUOXGfb0h","name":"Cyclone Approach","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Berserker","description":{"value":"Cyclone Approach \nThe Cyclone Approach empowers the berserker’s ability to fight with weapons in each hand. Followers of this approach learn to move quickly to avoid attacks and can become a whirlwind of fury and steel, cleaving through hordes of enemies.
\nDual Wielder \nWhen you choose this approach at 3rd level, when you engage in Two-Weapon Fighting, you can add your Strength or Dexterity modifier (your choice) to the damage of your Two-Weapon Fighting attack as long as it doesn’t already include that modifier.
\nDouble Swing \nAlso at 3rd level, once on each of your turns when you miss with an attack while raging, you can immediately make a melee attack with the weapon in your other hand.
\nTwisting Winds \nAt 6th level, your unpredictable movement makes you harder to hit and pin down. When you make a saving throw or ability checks to avoid being knocked prone, pushed, grappled, or restrained, it gains a bonus equal to your Strength or Dexterity modifier (your choice) as long as it doesn’t already include that modifier.
\nMighty Leap \nStarting at 10th level, the distance you can jump is doubled, and you do not provoke attacks of opportunity if you leave a hostile creature’s reach while jumping.
\nTornado \nAt 14th level, you can become a tornado of attacks. When you take the Attack action on your turn, you can forgo one of your regular attacks to make a melee attack against any number of creatures within 5 feet of you, with a separate attack roll for each target. If you are wielding a separate melee weapon in each hand, each successful hit against a target deals damage equal to the damage dice of both weapons + your ability modifier + any other modifiers.
\nYou can use this feature a number of times equal to your Strength modifier (a minimum of once). You regain all expended uses when you finish a short or long rest.
"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Cyclone%20Approach.webp","effects":[]}
{"_id":"c89hsFFZG4WGlYcV","name":"Zoologist Pursuit","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Scholar","description":{"value":"Zoologist Pursuit \nMany academics develop an affinity for nature, studying the vast fauna that inhabit the different planets throughout the galaxy. Those scholars who choose the Zoologist Pursuit capitalize on their knowledge of animals, developing a strong bond with a companion with whom they gain an advantage on the battlefield.
\nWilderness Expert \nWhen you choose this pursuit at 3rd level, you gain proficiency your choice of Animal Handling or Nature skills. Additionally, when you make a Wisdom (Animal Handling) check, you gain a bonus to the check equal to your Intelligence modifier.
\nBeast Companion \nAlso at 3rd level, you learn to employ all the knowledge you've accumulated to forge a powerful bond with your own personal beast companion.
\nChoose your beast, which is detailed at the end of this pursuit. Over the course of 8 hours, which can be done during a long rest, you can expend 500 cr worth of herbs and food to call forth an animal from the wilderness to serve as your companion.
\nIf your beast dies, or you want to bond with a different creature, you must first break the bond with your current beast companion. You may only have one beast companion at a time.
\nYour beast gains a variety of benefits while it is bonded to you:
\n\nThe beast obeys your commands as best it can. It acts on your turn, and you determine its actions, decisions, attitudes, and so on. If you are incapacitated or absent, your beast acts on its own. \nYour beast's level equals your scholar level, and for each scholar level you gain after 3rd, your beast companion gains an additional hit die and increases its hit points accordingly. \nYour beast has the proficiency bonus of a player character of the same level. \nWhenever you gain the Ability Score Improvement feature in this class, your beast's abilities also improve. Your beast can increase one ability score of your choice by 2, or it can increase two ability scores of your choice by 1. As normal, your beast can't increase an ability score above 20 using this feature unless its description specifies otherwise. \nWhile your beast is the target of your Critical Analysis feature, it gains a bonus to ability checks, armor class, attack rolls, damage rolls, and saving throws equal to half your Intelligence modifier (rounded up). \n \nAdditional Maneuvers \nLastly at 3rd level, you gain access to new maneuvers which reflect the progress of your studies into the biology and behavior of animals. Whenever you learn a new maneuver, you can choose from any of the following as well. The maneuvers are listed in alphabetical order.
\nLoyal Bond \nWhenever you are hit with an attack, you can expend one superiority die to command your companion to immediately use its reaction and move up to its speed directly towards you. If it ends this movement within 5 feet of you, roll the superiority die. Your companion takes the damage instead of you, subtracting the amount you rolled from the total.
\nGo Get 'Em \nWhile your companion is moving, you can expend a superiority die and add 5 times the number rolled to its movement speed.
\nPin Down \nWhen your beast attempts to grapple or knock a creature prone, you can expend a superiority die to give it direction as long as it can see or hear you. Roll a superiority die and add it to your beast's Strength (Athletics) check.
\nPrimal Endurance \nAs an action, you can expend a superiority die to improve your beast's defense. Roll the die and add it to your beast's AC until the beginning of your next turn.
\nSic 'Em \nAs an action, you can command your beast to savage a nearby enemy. Your beast can use its reaction to immediately move up to 10 feet and make one attack, adding the superiority dice to the damage roll on a hit.
\nSpine-Chilling Howls \nAs an action, you can expend one superiority die to command your beast to frighten another creature. The target must then succeed on a Wisdom saving throw against your Maneuver save DC or become frightened of both you and your beast for 1 minute.
\nWild Senses \nWhenever you make a Wisdom (Perception) or a Wisdom (Survival) check, you can request the aid of your beast by expending a superiority die, adding the number rolled to the check. You can use this maneuver before or after making the ability check, but before the results of the ability check are determined.
\nVicious Hunting \nBeginning at 6th level, your beast companion's strikes count as enhanced for the purpose of overcoming resistance and immunity to unenhanced attacks and damage.
\nCreature Comprehension \nStarting at 9th level, when your beast makes an attack roll, ability check, or saving throw, you may expend a superiority die and apply the benefits of a maneuver you know from this class, as if you have taken the action yourself.
\nFeral Ferocity \nOnce you've reached 17th level, you have learned how to push your beast beyond its limits. If your beast is within 30 feet of you and can see or hear you, you can command it to enter a furious state. While raging, your beast gains the following benefits:
\n\nYour beast has advantage on Strength checks and Strength saving throws if it is size Medium or larger. \nYour beast has advantage on Dexterity checks and Dexterity saving throws if it is size Small or smaller. \nWhen your hits with an attack, it deals bonus damage equal to your Intelligence modifier. \nYour beast has resistance to kinetic and energy damage. \n \nYour beast's furious state lasts for 1 minute. It ends early if your beast is knocked unconscious. You can end your beast's furious state as a bonus action.
\nOnce you've used this feature, you can't use it again until you finish a long rest.
\nDiscoveries (Zoologist) \nWhen you select this pursuit, you gain access to new discoveries which reflect your studies in biology and behaviour of alien lifeforms. Whenever you learn a new discovery, you can choose from any of the following as well. The discoveries are listed in alphabetical order.
\nAdvantageous Companion \nWhen you make a Charisma (Intimidation) check against a creature that can see your beast companion, and your companion is size Medium or larger, you make the check with advantage.
\nWhen you make a Charisma (Persuasion) check against a creature that can see your beast companion, and your companion is size Small or Tiny, you make the check with advantage.
\nColossal Companion \nPrerequisite: 15th level
\nYou can attempt to temporarily take control of a Huge beast. With the use of 10,000 cr worth of herbs and food, you can make a DC 15 Animal Handling check. On a success, the creature becomes your companion for 1d4 hours and gains the benefits of your Beast Companion feature. You can attempt to extend the duration by one hour by making additional Animal Handling checks. The DC for the first check is 20, and increases by 5 on each subsequent check. On a failure, the creature becomes hostile to you if it wasn't already and becomes immune to this feature for 24 hours.
\nHolocam Attachment \nYou have learned how to safely attach a holocam on the head of the companion. You learn the tracker droid interface tech power, and your beast becomes a valid target of this power.
\nNeat Tricks \nPrerequisite: 5th level
\nYour beast gains proficiency in one Strength or Dexterity skill of your choice. If your beast's size is Medium or larger and the chosen skill uses Strength, it has expertise in the chosen skill. If your beast's size is Small or smaller and the chosen skill uses Dexterity, it has expertise in the chosen skill.
\nProtective Friend \nIf a creature makes a melee attack against you or your companion, and your companion is within 5 feet of you, you can use your reaction to impose disadvantage on the attack roll.
\nThe More The Merrier \nPrerequisite: 7th level
\nWhenever you attempt to call forth an animal as your companion, you can instead spend 1,000 cr worth of components and call forth a swarm of Tiny creatures. The swarm is composed of a number of creatures equal to your scholar level + your Intelligence modifier, and is size Medium. All of the creatures within the swarm act as a single creature.
\nGenerating Your Beast \nChoosing your beast companion is an integral part of being a Zoologist Scholar. Your beast takes a form of your choosing. Alternatively, your GM can choose what form your beast takes based on your environment.
\nOnce you've selected your type of beast, you assign your beast companion's ability scores using standard array (16, 14, 14, 12, 10, 8) as you see fit.
\nBeast Features \nAll beasts share the following traits.
\nHit Points \n\nHit Dice: 1d4 per beast companion level \nHit Points at 1st Level: 4 + your beast's Constitution modifier \nHit Points at Higher Levels: 1d4 (or 3) + your beast's Constitution modifier per beast level after 1st \n \nProficiencies \n\nLanguages: Your beast can understand simple commands spoken in two languages of your choice, as well as hand signals, but it can not speak \nSaving Throws: Choose one from Strength, Intelligence, or Charisma, and another from Dexterity, Constitution, or Wisdom \nSkills: Choose two from Acrobatics, Athletics, Intimidation, Perception, Performance, Survival, and Stealth \n \nFeatures \n\nArmor Class: 10 + your beast's Dexterity modifier \nBestial Traits: Your beast companion four bestial traits of your choice. It gains an additional trait at 5th level (5), 8th level (6), 11th level (7), 14th level (8), and 17th level (9). Whenever you gain a level in this class, you can exchange one trait for another one. \nNatural Weapon: Your beast companion attacks with a natural weapon, such as claws or a bite. On a hit, it deals 1d4 kinetic damage. \nSize: Tiny \nSpeed: 20 ft. \nType: Beast \n \nBestial Traits \nThe traits are presented in alphabetical order.
\nAerial \nYour beast companion has a flying speed equal to its walking speed, and opportunity attacks made against it have disadvantage.
\nAmphibious \nYour beast companion has a swimming speed equal to its walking speed, and it can breathe air and water.
\nBurrower \nYour beast companion has a burrowing speed equal to its walking speed, and it has blindsight out to 10 feet.
\nCharger \nIf your beast moves at least half its speed straight towards a target before making a melee attack, it deals an additional 1d8 damage on a hit.
\nClimber \nYour beast companion has a climbing speed equal to its walking speed, and it has advantage on Strength saving throws and Strength (Athletics) checks that involve climbing.
\nDarkvision \nYour beast companion is accustomed to low-light environments. Your beast can see in dim light within 60 feet as if it were bright light, and in darkness as if it were dim light. Your beast can't discern color in darkness, only shades of gray.
\nEvasive \nYour beast companion can take the Disengage action as a bonus action.
\nForce Adept \nPrerequisite: Force Sensitive Your beast companion knows one 2nd-level force power of your choice, and once per long rest it can cast it at 2nd-level without expending force points. Your beast's forcecasting ability is Wisdom or Charisma (depending on power alignment).
\nForce Resistance \nYour beast companion has advantage on saving throws against force powers.
\nForce Sensitive \nYour beast companion knows one 1st-level force power of your choice, and once per long rest it can cast it at 1st-level without expending force points. Your beast's forcecasting ability is Wisdom or Charisma (depending on power alignment).
\nGrappler \nWhen your beast hits with a melee weapon attack, it can use a bonus action to attempt to grapple the target. On a success, the target is both grappled and restrained, and your beast can't attack again while it has a creature grappled.
\nHeavy Hide \nYour beast companion's armor class becomes 14.
\nKeen Hearing \nYour beast companion has advantage on Wisdom (Perception) checks that rely on hearing.
\nKeen Sight \nYour beast companion has advantage on Wisdom (Perception) checks that rely on sight.
\nKeen Smell \nYour beast companion has advantage on Wisdom (Perception) checks that rely on smell.
\nLight Hide \nYour beast companion's armor class becomes 11 + it's Dexterity modifier.
\nMedium Hide \nYour beast companion's armor class becomes 13 + it's Dexterity modifier, to a maximum of +2.
\nNatural Camouflage \nWhen your beast companion attempts to hide, it can opt to not move on its turn. If it avoids moving, it is considered lightly obscured until it moves.
\nNimble Weapon \nYour beast companion can use Dexterity instead of Strength for its attack and damage rolls.
\nPack Tactics \nYour beast companion has advantage on an attack roll against a creature if at least one ally of your beast companion is within 5 feet of the creature and the ally isn't incapacitated.
\nPouncer \nIf your beast moves at least half its speed straight toward a creature and hits it with a melee attack, the creature must make a Strength saving throw (DC = 8 + your beast's proficiency bonus + its Strength modifier). If the creature is larger than your beast, it makes this save with advantage. On a failed save, the creature is knocked prone, and your beast can make one additional attack against it as a bonus action.
\nPowerful Build \nYour beast companion counts as one size larger when determining its carrying capacity and the weight it can push, drag, or lift.
\nRampager \nIf your beast reduces a creature to 0 hit points with a melee attack on its turn, your beast and take a bonus action to move up to half its speed and make a melee attack.
\nRanged Weapon \nYour beast companion has a natural ranged weapon, such as a spitter or or tail spikes. It has a normal range of 30 feet and a long range of 90 feet, and on a hit it deal kinetic damage equal to its natural weapon damage die.
\nReach Weapon \nYour beast companion has a natural weapon with reach, such as a tail or wings. It has the reach property, and on a hit it deals kinetic damage equal to its natural weapon damage die.
\nSize: Huge \nPrerequisite: Size Large Your beast companion’s size is Huge. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d12, its natural weapon damage die becomes a d12, and its walking speed increases to 40.
\nSize: Large \nPrerequisite: Size Medium Your beast companion's size is Large. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d10, its natural weapon damage die becomes a d10, and it's walking speed increases to 35.
\nSize: Medium \nPrerequisite: Size Small Your beast companion's size is Medium. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d8, its natural weapon damage die becomes a d8, and its walking speed increases to 30.
\nSize: Small \nYour beast companion's size is Small. Its hit points increase by an amount equal to its level + 1, its Hit Die becomes a d6, its natural weapon damage die becomes a d6, and it's walking speed increases to 25.
\nSturdy-Legged \nYour beast companion's long jump is up to 20 feet and its high jump is up to 10 feet, with or without a running start, and it has advantage on Strength and Dexterity saving throws made against effects that would knock it prone.
\nSwift \nYour beast companion can take the Dash action as a bonus action.
\nTremorsense \nYour beast companion gains tremorsense out to 30 feet.
\nVenomous Weapon \nWhen your beast companion deals damage to a creature, it must make a Constitution saving throw (DC = 8 + your beast's proficiency bonus + your beast's Constitution modifier) or become poisoned until the end of its next turn.
"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":""},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Zoologist%20Pursuit.webp","effects":[]}
{"_id":"cROcc25Zj1MT6Yf6","name":"Form I: Shii-Cho","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"archetype","data":{"className":"Guardian","description":{"value":"Form I: Shii-Cho \nForm I, also known as Determination Form, uses wild, unpredictable attacks designed to distract and disarm their foes. Those guardians who focus on Shii-Cho Form make seemingly random, yet deliberate, attacks to knock their opponents off-balance.
\nForm Basics \nWhen you choose this form as your focus at 3rd level, you learn the basics of the chosen form. You gain the @Compendium[sw5e.lightsaberform.rUL9jO0rPLWqOliO]{Shii-Cho Lightsaber Form}, detailed in Chapter 6 of the Player’s Handbook. If you already know this form, you can instead choose another lightsaber form. You can't take a lightsaber form option more than once, even if you later get to choose again.
\nThe Way of the Sarlaac \nAlso at 3rd level, as a bonus action, you can enter a frenetic stance for one minute. While in this stance, the first time you hit a creature with a melee weapon attack on your turn, it has disadvantage on the next melee attack roll it makes against you before the start of your next turn. Additionally, if that creature is within 5 feet of you, it must make a Strength saving throw (DC = 8 + your proficiency bonus + your Strength or Dexterity modifier). On a failed save, it is pushed back 5 feet, and you can immediately move into the space it just vacated without provoking opportunity attacks.
\nThis effect ends early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.
\nChannel the Force \nLastly at 3rd level, you gain the following Channel the Force option.
\nDisarming Slash \nWhen you hit a creature with a melee weapon attack, you can expend a use of your Channel the Force (no action required) to attempt to disarm the target, forcing it to drop one item of your choice that it's holding. The creature must make a Strength saving throw. On a failed save, it drops the object you choose. If you are within 5 feet of the target, and you have a free hand, you can catch the item. Otherwise, the object lands at its feet.
\nUnpredictable Motion \nBeginning at 7th level, while you are wielding a melee weapon, opportunity attacks against you are made at disadvantage.
\nSarlaac Sweep \nStarting at 15th level, when a creature moves to within 5 feet of you, you can use your reaction to make a melee weapon attack against that creature. If the attack hits, you can attempt to damage another creature within 5 feet of the original target and within your reach. If the original attack roll would hit the second creature, it takes damage equal to your Strength or Dexterity modifier (your choice). The damage is of the same type dealt by the original attack.
\nMaster of Determination \nAt 20th level, the erratic fluidity of your movement confounds even the most determined of foes. Your Strength or Dexterity and Wisdom or Charisma scores (your choice) increase by 2. Your maximum for these scores increases by 2. Additionally, you can use your action to gain the following benefits for 1 minute:
\n\nYou have resistance to kinetic and energy damage from unenhanced weapons. \nAttack rolls made against you can't have advantage. \nWhen more than one creature is within 5 feet of you, you gain a bonus to your Armor Class equal to the number of creatures within 5 feet of you, up to your Wisdom or Charisma modifier (your choice, minimum of one). \nWhen you use your Sarlaac Sweep feature, you have advantage on the attack roll, and you can apply the bonus damage to every creature within 5 feet of you. \n \nThis effect ends early if you are incapacitated or die. Once you've used this feature, you can't use it again until you finish a long rest.
"},"source":"EC","activation":{"cost":null,"type":""},"actionType":"","damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"classCasterType":"Forcecaster"},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Archetypes/Shii-Cho%20Form.webp","effects":[]}
diff --git a/packs/packs/forcepowers.db b/packs/packs/forcepowers.db
index 99767264..062b9b3a 100644
--- a/packs/packs/forcepowers.db
+++ b/packs/packs/forcepowers.db
@@ -8,6 +8,7 @@
{"_id":"28uF6yN4NLoc1Mf7","name":"Master Speed","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Knight Speed
\nChoose up to two willing creatures that you can see within range. Until the power ends, each targets’ speed is doubled, they gain a +2 bonus to AC, they have advantage on Dexterity saving throws, and they gain an additional action on each of their turns. That action can be used only to take the Attack (one weapon attack only), Dash, Disengage, Hide, or the Use an Object Action.
\nWhen the power ends, each target can’t move or take actions until after its next turn, as a wave of lethargy sweeps over it.
\nForce Potency. When you cast this power using a force slot of 8th-level or higher, you can target one additional creature for each slot level above 7th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":2,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":7,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Master%20Speed.webp","effects":[]}
{"_id":"2EdyvNwmyRD9yhmn","name":"Maddening Darkness","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Shroud of Darkness
\nTerrifying darkness spreads from a point you choose within range to fill a 60-foot-radius sphere until the power ends. The darkness spreads around corners. A creature with darkvision can’t see through this darkness. Unenhanced light, as well as light created by powers of 8th level or lower, can’t illuminate the area.
\nShrieks, gibbering, and mad laughter can be heard within the sphere. Whenever a creature starts its turn in the sphere, it must make a Wisdom saving throw, taking 8d8 psychic damage on a failed save, or half as much damage on a successful one.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Shroud of Darkenss"},"duration":{"value":10,"units":"minute"},"target":{"value":60,"units":"ft","type":"radius"},"range":{"value":150,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["8d8","psychic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":8,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Maddening%20Darkness.webp","effects":[]}
{"_id":"2MaAEt5XSnjEurWK","name":"Valor","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Resistance
\nYou bless up to three creatures of your choice within range. Whenever a target makes an attack roll or a saving throw before the power ends, the target can roll a d4 and add the number rolled to the attack roll or saving throw.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":3,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Valor.webp","effects":[]}
+{"name":"Wakefulness","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"Prerequisite: Breath Control
\nYou use the Force to control your body's functions. For the duration, you gain the following benefits:
\n\nYou ignore the effects of any levels of exhaustion you have. \nYou do not need to eat, drink, or sleep. You can't be forced to sleep by any means. To gain the benefits of a long rest, you can spend all 8 hours doing light activity, such as reading and keeping watch. \n \nIf you still have any levels of exhaustion when this power ends, you must make a DC 15 Constitution saving throw. On a failed save, you gain one level of exhaustion.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":8,"units":"hour"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.ROvBWSja1cjiDOO0"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Wakefulness.webp","effects":[],"_id":"2obXjKljRPeuoqcl"}
{"_id":"3IoaQSSoAtFsoavO","name":"Master Force Barrier","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Improved Force Barrier
\nThis power massively bolsters your allies with toughness and resolve. Creatures of your choice in a 30-foot radius around you when you cast this power gain the following benefits:
\n\nThe creature sheds dim light in a 5-foot radius. \nThe creature has advantage on all saving throws \nOther creatures have disadvantage on attack rolls against them. \nWhen a dark side creature hits them with a melee attack, that creature must make a Constitution saving throw or be blinded until the power ends. \n ","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":8,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Master%20Force%20Barrier.webp","effects":[]}
{"_id":"3KGdCbHATzaghiwo","name":"Force Propel","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Push/Pull
\nChoose one or more creatures or objects not being worn or carried within 60 feet that weigh up to a combined total of 15 pounds. The creatures or objects immediately move 60 feet in a direction of your choice. If the creatures or objects end this movement in the air, they immediately fall to the ground. If the creatures or objects collide with any one target during its travel, both the creatures or objects and the target take 3d8 kinetic damage. If the target is a creature, it must make a Dexterity saving throw. On a failed save, it takes 3d8 kinetic damage, or half as much on a successful one.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, the maximum weight increases by 15 pounds and the damage increases by 1d8 for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":null,"units":"any","type":"object"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d8","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d8"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Propel.webp","effects":[]}
{"_id":"3odfJsD1RezuxzDG","name":"Mass Hysteria","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Hysteria
\nDrawing on the deepest fears of a group of creatures, you create illusory creatures in their minds, manifesting their worst nightmares as an implacable threat visible only to them. Each creature in a 30-foot-radius sphere is frightened for the duration of the power. At the end of each of the frightened creature’s turns, it must succeed on a Wisdom saving throw or take 5d10 psychic damage. On a successful save, the power ends for that creature. This power has no effect on droids or constructs.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Hysteria Power"},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["5d8","psychic"]],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":9,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Mass%20Hysteria.webp","effects":[]}
@@ -49,10 +50,12 @@
{"_id":"C2ROn2KK2QTLm9DW","name":"Guidance","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You touch one willing creature. Once before the power ends, the target can roll a d4 and add the number rolled to one ability check of its choice. It can roll the die before or after making the ability check. The power then ends.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Guidance.webp","effects":[]}
{"_id":"C5UBI7f4LoNg2dPM","name":"Force Meld","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Whisper
\nYou forge a telepathic link among up to eight willing creatures of your choice within range, psychically linking each creature to all the others for the duration. Droids, constructs, and creatures with Intelligence scores of 2 or less aren’t affected by this power.
\nUntil the power ends, the targets can communicate telepathically through the bond whether or not they have a common language. The communication is possible over any distance, though it can’t extend beyond a single planet.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":8,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Meld.webp","effects":[]}
{"_id":"CJR5SlWKoaCtY1bq","name":"Spare the Dying","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You touch a living creature that has 0 hit points. The creature becomes stable. This power has no effect on droids or constructs.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Spare%20the%20Dying.webp","effects":[]}
+{"name":"Telepathic Link","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Whisper
\nYou establish a telepathic link with one willing humanoid you touch. Until the power ends, whenever you and the target can see each other, each of you can communicate with the other via telepathy.
\nYou don’t need to share a language with a creature for it to understand your telepathic utterances, and the creature understands you even if it lacks a language. The creature can respond to you telepathically as well, but it must understand at least one language in order to communicate this way.
\nForce Potency. When you cast this power using a force slot of 3rd level or higher, the duration increases to Concentration, up to 1 hour, and if either you or the creature you are linked to are surprised, while both of you can see each other, the surprised creature can still act normally during the surprise round, as if it were not surprised.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.ribj4iIyIwS74McS"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Telepathic%20Link.webp","effects":[],"_id":"CjHXQcv4UPNIcIg0"}
{"_id":"CkMXwj0l0H0rD9md","name":"Mass Malacia","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"Prerequisite: Malacia
\nEach creature in a 30-foot cube within range must make a Wisdom saving throw. On a failed save, the creature becomes charmed for the duration. While charmed by this power, the creature is incapacitated and has a speed of 0.
\nThe power ends for an affected creature if it takes any damage or if someone else uses an action to shake the creature out of its stupor. This power has no effect on droids or constructs.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"cube"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":3,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Mass%20Malacia.webp","effects":[]}
{"_id":"CkwLRMWRdXp1ZOIh","name":"Greater Saber Throw","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"Prerequisite: Improved Saber Throw
\nAs a part of the action used to cast this power, you must make a ranged force attack with a lightweapon or vibroweapon against one target within the power’s range, otherwise the power fails. On a hit, the target takes 6d8 damage of the same type as the weapon’s damage and must make a Constitution saving throw against an additional effect depending on your choice of casting ability:
\nWisdom. The target takes an additional 4d6 force damage and it gains four levels of slowed until the end of its next turn. On a success, the target takes half as much damage and its speed isn’t reduced.
\nCharisma. The target takes an additional 4d6 necrotic damage and cannot regain hit points until the end of its next turn. On a success, the target takes half as much damage and its healing capability is unaffected.
\nThe weapon then immediately returns to your hand.
\nForce Potency. When you cast this power using a force slot of 6th level or higher, the force or necrotic damage increases by 1d6 for each slot above 5th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["6d8",""]],"versatile":""},"formula":"4d6","save":{"ability":"con","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Greater%20Saber%20Throw.webp","effects":[]}
{"_id":"CsLjEyQtegNiXKIZ","name":"Tremor","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Burst
\nYou cause a tremor in the ground within range. Each creature other than you in a 5-foot-radius sphere centered on that point must make a Dexterity saving throw. On a failed save, a creature takes 1d6 kinetic damage and is knocked prone. On a successful save, the creature takes half as much damage and isn’t knocked prone. If the ground in that area is loose earth or stone, it becomes difficult terrain until cleared, with each 5-foot-diameter portion requiring at least 1 minute to clear by hand.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, the damage increases by 1d6 for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":5,"units":"ft","type":"radius"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Tremor.webp","effects":[]}
{"_id":"CuAbwhIt3j2V30Ey","name":"Saber Reflect","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"In response to being attacked, you raise your weapon to attempt to deflect. When you you use this power, the damage you take from the attack is reduced by 1d6. If you reduce the damage to 0, you’re wielding a lightweapon or vibroweapon, and the damage is energy or ion, you can reflect the attack at a target within range as part of the same reaction. Make a ranged force attack at a target you can see. The attack has a normal range of 20 feet and a long range of 60 feet. On a hit, the target takes the triggering attack’s normal damage.
\nIf you would have resistance to the triggering damage, resistance is applied before the damage reduction.
\nThe power’s damage reduction increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take in response to being hit by a ranged attack"},"duration":{"value":null,"units":"inst"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d6","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Saber%20Reflect.webp","effects":[]}
+{"name":"Tapas","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"You use the Force to inure yourself to the elements. For the duration, you are considered adapted to hot and cold climates.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":24,"units":"hour"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.Heu6br9Yn7ZOVnnA"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Tapas.webp","effects":[],"_id":"D9V8ShFjzvenV8ud"}
{"_id":"DFpooO6icOfTdG5C","name":"Master Feedback","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Greater Feedback
\nYou unleash the power of your mind to blast the intellect of up to ten creatures of your choice that you can see within range. Creatures that have an Intelligence score of 2 or lower are unaffected.
\nEach target must make an Intelligence saving throw. On a failed save, a target takes 14d6 psychic damage and is stunned. On a successful save, a target takes half as much damage and isn’t stunned.
\nA stunned target can make a Wisdom saving throw at the end of each of its turns. On a successful save, the stunning effect ends.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Greater Feedback Power"},"duration":{"value":null,"units":"inst"},"target":{"value":10,"units":"","type":"creature"},"range":{"value":90,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["14d6","psychic"]],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":8,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Master%20Feedback.webp","effects":[]}
{"_id":"ENNhFJKCcxrg481J","name":"Improved Phasestrike","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Phasestrike
\nChoose up to five creatures you can see within range. Make a melee force attack against each one. On hit, a target takes 6d10 force damage. You can then teleport to an unoccupied space you can see within 5 feet of one of the creatures you chose.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":5,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"mpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["6d10","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Improved%20Phasestrike.webp","effects":[]}
{"_id":"EXK4sg6gKXfMnLnf","name":"Battle Meditation","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You exude an aura out to 5 feet that boosts the morale and overall battle prowess you and your allies while simultaneously reducing the opposition’s combat-effectiveness by eroding their will to fight.
\nWhenever you or a friendly creature within your meditation makes an attack roll or a saving throw, they can roll a d4 and add the number rolled to the attack roll or saving throw.
\nWhenever a hostile creature enters your meditation or starts its turn there, it must make a Charisma saving throw. On a failed save, it must roll a d4 and subtract the number rolled from each attack roll or saving throw it makes before the end of your next turn. On a successful save, it is immune to this power for 1 day.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":5,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"cha","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Battle%20Meditation.webp","effects":[]}
@@ -67,7 +70,7 @@
{"_id":"G8UVHP4MXW6Dudky","name":"Phasestrike","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Until the power ends, your movement doesn’t provoke opportunity attacks.
\nOnce before the power ends, you can give yourself advantage on one weapon attack roll on your turn. That attack deals an extra 1d8 force damage on a hit. Whether you hit or miss, your walking speed increases by 30 feet until the end of that turn.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Phasestrike.webp","effects":[]}
{"_id":"GBJRx32gU9xU1Q6s","name":"Improved Feedback","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Feedback
\nYou unleash a blast of psychic energy at a target within range. If the target can hear you (though it need not understand you), it must succeed on an Intelligence saving throw. On a failed save, it takes 3d6 psychic damage and must immediately use its reaction, if available, to move as far as its speed allows away from you. The creature doesn’t move into obviously dangerous ground, such as a fire or a pit. On a successful save, the target takes half as much damage and doesn’t have to move away. A deafened creature automatically succeeds on the save.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, the damage increases by 1d6 for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Feedback Power"},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["3d6","psychic"]],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Improved%20Feedback.webp","effects":[]}
{"_id":"H51uSnFY6eyBTXRJ","name":"Stasis","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Stun
\nChoose a creature that you can see within range. The target must succeed on a Wisdom saving throw or be paralyzed for the duration. At the end of each of its turns, the target can make another Wisdom saving throw. On a success, the power ends on the target. This power has no effect on droids or constructs.
\nForce Potency. When you cast this power using a force slot of 6th level or higher, you can target an additional creature for each slot level above 5th. The creatures must be within 30 feet of each other when you target them.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":90,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":5,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Stasis.webp","effects":[]}
-{"_id":"HPtgWP83jKSyFWFp","name":"Aura of Purity","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Restoration
\nPurifying energy radiates from you in a 30-foot radius. Until the power ends, the aura moves with you, centered on you. Each nonhostile creature in the aura (including you) can’t become diseased, has resistance to poison damage, and has advantage on saving throws against effects that cause any of the following conditions: blinded, charmed, deafened, frightened, paralyzed, poisoned, and stunned.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Aura%20of%20Purity.webp","effects":[]}
+{"_id":"HPtgWP83jKSyFWFp","name":"Aura of Purity","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Restoration
\nPurifying energy radiates from you in a 30-foot radius. Until the power ends, the aura moves with you, centered on you. Each nonhostile creature in the aura (including you) can’t become diseased, has resistance to poison damage, and has advantage on saving throws against effects that cause any of the following conditions: blinded, charmed, deafened, frightened, paralyzed, poisoned, and stunned.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Aura%20of%20Purity.webp","effects":[]}
{"_id":"Hgn1yHJsMzp5qKr5","name":"Force Reflect","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"Prerequisite: Saber Reflect
\nIn response to being attacked, you attempt to deflect the attack with the Force. When you use this power, the damage you take from the attack is reduced by 1d10. If you reduce the damage to 0 and the damage is energy, force, ion, kinetic, lightning, necrotic, or sonic, you can reflect the attack at a target within range as part of the same reaction. Make a ranged force attack at a target you can see. The attack has a normal range of 30 feet and a long range of 90 feet. On a hit, the target takes the triggering attack’s normal damage.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, the damage reduction increases by 1d10 for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take in response to being hit by a ranged attack"},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d10","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d10"}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Reflect.webp","effects":[]}
{"_id":"HqxcYlFOZJZ91a98","name":"Precognition","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Danger Sense
\nYour mastery of the Force gives you a limited ability to see into the immediate future. For the duration, you can’t be surprised and you have advantage on attack rolls, ability checks, and saving throws. Additionally, other creatures have disadvantage on attack rolls against you for the duration.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":1,"condition":""},"duration":{"value":8,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":9,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Precognition.webp","effects":[]}
{"_id":"HvyTel83im9br37p","name":"Force Leap","permission":{"default":0,"XA86Wm4MjngMySbc":3},"type":"power","data":{"description":{"value":"Until the end of your next turn, you can use your forcecasting ability score instead of your Strength score when you jump, and always count as having made a running start before jumping.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":null,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Leap.webp","effects":[]}
@@ -100,7 +103,7 @@
{"_id":"NrMpYXddHnoPKHT2","name":"Force Lightning Cone","permission":{"default":0,"9BhUyjgxIxogl2ot":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Chain Lightning
\nLightning arcs from your hands. Each creature in a 60-foot cone must make a Dexterity saving throw. A creatures takes 12d6 lightning damage on a failed save, or half as much on a successful one.
\nForce Potency. When you cast this power using a force slot of 8th level or higher, the damage increases by 2d6 for each slot level above 7th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":60,"units":"ft","type":"cone"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["12d6","lightning"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":7,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":"2d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Cone%20Lightning.webp","effects":[]}
{"_id":"Ns34rjyKzqeydYBH","name":"Sense Emotion","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You attune your senses to pick up the emotions of others for the duration. When you cast the power, and as your action on each turn until the power ends, you can focus your senses on one humanoid you can see within 30 feet of you. You instantly learn the target’s prevailing emotion, whether it’s love, anger, pain, fear, calm, or something else. If the target isn’t actually humanoid or it is immune to being charmed, you sense that it is calm.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Sense%20Emotion.webp","effects":[]}
{"_id":"O5J8TTpTW7eHLDgG","name":"Sense Force","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"For the duration, you sense the use of the Force, or its presence in an inanimate object within 30 feet of you. If you sense the Force in this way, you can use your action to determine the direction from which it originates and, if it’s in line of sight, you see a faint aura around the person or object from which the Force emanates.
\nForce Potency. When you cast this power using a 3rd-level force slot, the range increases to 60 feet. When you use a 5th-level force slot, the range increases to 500 feet. When you use a 7th-level force slot, the range increases to 1 mile. When you use a 9th-level force slot, the range increases to 10 miles.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Sense%20Force.webp","effects":[]}
-{"_id":"OGLh7pBroWR6te2k","name":"Armor of Abeloth","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"A protective force surrounds you, manifesting as shimmering light that covers you and your gear. You gain 5 temporary hit points for the duration. If a creature hits you with a melee attack while you have these hit points, the creature takes 5 psychic damage.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, both the temporary hit points and the psychic damage increase by 5 for each slot.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Armor%20of%20Abeloth.webp","effects":[]}
+{"_id":"OGLh7pBroWR6te2k","name":"Armor of Abeloth","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"A protective force surrounds you, manifesting as shimmering light that covers you and your gear. You gain 5 temporary hit points for the duration. If a creature hits you with a melee attack while you have these hit points, the creature takes 5 psychic damage.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, both the temporary hit points and the psychic damage increase by 5 for each slot.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Armor%20of%20Abeloth.webp","effects":[]}
{"_id":"OYQAqS6rjVy6o5Oa","name":"Kill","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Ruin
\nYou compel one creature you can see within range to die instantly. If the creature you choose has 100 hit points or fewer, it dies. Otherwise, the power has no effect.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Ruin Power"},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"Have over 100 HP left or Die!","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":9,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Kill.webp","effects":[]}
{"_id":"Oxb3dpusa4SmIKQd","name":"Improved Battle Meditation","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Battle Meditation
\nYou exude an aura out to 15 feet that boosts the morale and overall battle prowess you and your allies while simultaneously reducing the opposition’s combat-effectiveness by eroding their will to fight.
\nWhenever you or a friendly creature within your meditation makes an attack roll or a saving throw, they can roll a d6 and add the number rolled to the attack roll or saving throw.
\nWhenever a hostile creature enters your meditation or starts its turn there, it must make a Charisma saving throw. On a failed save, it must roll a d6 and subtract the number rolled from each attack roll or saving throw it makes before the end of your next turn. On a successful save, it is immune to this power for 1 day.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":15,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d6","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Improved%20Battle%20Meditation.webp","effects":[]}
{"_id":"PUHY0RrLsi4tAnZx","name":"Force Sight","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Sense Force
\nYou shift your vision to see through use of the Force; colors fade and inanimate objects appear as shades of gray. You gain the following benefits.
\n\nLiving things glow with the power of the Force. Those with an affinity for the light side glow blue, those with an affinity for the dark side glow red, and those with no attunement to either side of the Force glow yellow. How bright they glow is determined by how strong their connection to the Force is. \nYou gain blindsight to 30 feet. \nYou have advantage on Wisdom (Perception) checks that rely on sight against living targets within 30 feet. \n ","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Sight.webp","effects":[]}
@@ -122,7 +125,7 @@
{"_id":"VIi9ZD8Ha669UYRw","name":"Insanity","permission":{"default":0,"9BhUyjgxIxogl2ot":3},"type":"power","data":{"description":{"value":"Prerequisite: Horror
\nThis power assaults and twists creatures’ minds, spawning delusions and provoking uncontrolled action. Each creature in a 30-foot-radius sphere centered on you must succeed on a Wisdom saving throw when you cast this power or be affected by it.
\nAn affected target can’t take reactions and must roll a d8 at the start of each of its turns to determine its behavior for that turn. This power has no effect on constructs or droids.
\n\n\n\nd8 \nBehavior \n \n\n1 \nThe creature uses all its movement to move in a random direction. To determine the direction, roll a d8 and assign a direction to each die face. The creature doesn’t take an action this turn. \n \n\n2-6 \nThe creature doesn’t move or take actions this turn. \n \n\n7-8 \nThe creature uses its action to make a melee attack against a randomly determined creature within its reach. If there is no creature within its reach, the creature does nothing this turn. \n \n \n
\nAt the end of each of its turns, an affected target can make a Wisdom saving throw. If it succeeds, this effect ends for that target.
\nForce Potency. When you cast this power using a power slot of 6th level or higher, the radius of the sphere increases by 5 feet for each force slot level above 5th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":30,"units":"ft","type":"sphere"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d8","save":{"ability":"wis","dc":null,"scaling":"power"},"level":5,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Insanity.webp","effects":[]}
{"_id":"Vsnr0aVZQIcRD0Ed","name":"Force Mask","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Mind Trick
\nUntil the power ends or you use an action to dismiss it, you can disguise yourself through use of the Force in many ways. You can appear to be shorter or taller by about a foot and change the appearance of your body and weight, but you cannot change the basic structure of your body. This effect can include your clothes, weapons, and other belongings on your person.
\nThis effect is only visual, so any sort of physical contact will only interact with the real size and shape of you. A creature that uses its action to examine you can identify this effect with a successful Intelligence (Investigation) check against your force save DC. This power has no effect on droids or constructs.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"abil","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Mask.webp","effects":[]}
{"_id":"WBNy29tnkclypOVZ","name":"Danger Sense","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You put your faith in the Force, feeling out the future and seeing whether your actions will lead to fortune or ruin. The GM chooses from the following possible omens:
\n\nPeace, for results which are not dangerous \nDanger, for results which are dangerous but perhaps still worth the danger \nRuin, for results which are certain to end in death or tragedy \n \nThe power doesn’t take into account any possible circumstances that might change the outcome, such as the use of additional powers or the loss or gain of a companion.
\nIf you use this power two or more times before completing your next long rest, there is a cumulative 25 percent chance for each casting after the first that you get a neutral result regardless of the actual outcome.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Danger%20Sense.webp","effects":[]}
-{"_id":"WG79exTllchDSK6M","name":"Animate Weapon","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Disarm
\nYou select a melee weapon you wield, or one melee weapon within range that is not worn or carried by a conscious creature, and use the Force to cause it to levitate, acting as an extension of your will for the duration or until you cast this power again. When you use this power, you can cause the weapon to move up to 20 feet and make a melee force attack against a creature within 5 feet of it. On a hit, the target takes 1d8 + your forcecasting ability modifier damage. The type is of the normal damage dealt by the weapon.
\nWhile the weapon is animated, on each of your turns you can use a bonus action to move the weapon up to 20 feet and repeat the attack against a creature within 5 feet of it. At any time, you can end this force power to return the animated weapon to your hand.
\nAn enemy can attempt to gain control of the weapon by using its action to make a Strength (Athletics) check against your force save DC. On a success, the creature gains control of the weapon and the power ends.
\nForce Potency. When you cast this power using a force slot of 3rd level or higher, the weapon’s damage increases by 1d8 for every two slot levels above 2nd.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"object"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"wis","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Animate%20Weapon.webp","effects":[]}
+{"_id":"WG79exTllchDSK6M","name":"Animate Weapon","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Disarm
\nYou select a melee weapon you wield, or one melee weapon within range that is not worn or carried by a conscious creature, and use the Force to cause it to levitate, acting as an extension of your will for the duration or until you cast this power again. When you use this power, you can cause the weapon to move up to 20 feet and make a melee force attack against a creature within 5 feet of it. On a hit, the target takes 1d8 + your forcecasting ability modifier damage. The type is of the normal damage dealt by the weapon.
\nWhile the weapon is animated, on each of your turns you can use a bonus action to move the weapon up to 20 feet and repeat the attack against a creature within 5 feet of it. At any time, you can end this force power to return the animated weapon to your hand.
\nAn enemy can attempt to gain control of the weapon by using its action to make a Strength (Athletics) check against your force save DC. On a success, the creature gains control of the weapon and the power ends.
\nForce Potency. When you cast this power using a force slot of 3rd level or higher, the weapon’s damage increases by 1d8 for every two slot levels above 2nd.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"object"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"ability":"wis","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Animate%20Weapon.webp","effects":[]}
{"_id":"WWCvfD4ldEZoR5fn","name":"Mind Trap","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Force Confusion
\nYou attempt to trap the mind of your target in a psychic cage. The target must make a Charisma saving throw. On a failed save, the creature’s mind is trapped. It can think, but it can’t have any contact with or perceive the outside world. If the creature takes damage, it makes another Charisma save. On a success, the power ends. This power has no effect on droids or constructs.
\nForce Potency. When you cast this power using a force slot of 6th level or higher, after 1 minute of concentration the power’s duration becomes 24 hours and it no longer requires your concentration.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"cha","dc":null,"scaling":"power"},"level":4,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Mind%20Trap.webp","effects":[]}
{"_id":"XJSLZaP9Nix5EJHY","name":"Curse","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Denounce
\nUp to three creatures of your choice that you can see within range must make Charisma saving throws. Whenever a target that fails this saving throw makes an attack roll or a saving throw before the power ends, the target must roll a d4 and subtract the number rolled from the attack roll or saving throw.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, you can target one additional creature for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Have Denounce Power"},"duration":{"value":1,"units":"minute"},"target":{"value":3,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"cha","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 Addition Target"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Curse.webp","effects":[]}
{"_id":"XYHAKmU4gHSzRK3I","name":"Force Suppression","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Choose one creature, object, or force effect within range. Any force power of 3rd level or lower on the target ends. For each force power of 4th level or higher on the target, make an ability check using your forcecasting ability. The DC equals 10 + the power’s level. On a success, the power ends.
\nForce Potency. When you cast this power using a force slot of 4th level or higher, you automatically end the effects of a force power on the target if the power’s level is equal to or less than the level of the force slot you used.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"abil","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"wis","dc":null,"scaling":"power"},"level":3,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Suppression.webp","effects":[]}
@@ -157,6 +160,7 @@
{"_id":"ebWGew0oR2CG7S43","name":"Disperse Force","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Saber Ward
\nThis power absorbs damage from incoming energy attacks, lessening its effect on you and distributing it throughout your body. You have resistance to the triggering damage type until the start of your next turn. Also, you gain 5 temporary hit points to potentially absorb the attack. These temporary hit points last until the start of your next turn.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, the temporary hit points increases by 5 for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take when you take cold, energy, fire, ion, lightning, or sonic damage"},"duration":{"value":1,"units":"round"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"5 additional Temp HP"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Disperse%20Force.webp","effects":[]}
{"_id":"ecDdTd9MW0z4h8Hb","name":"Sap Vitality","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Make a melee force attack against a creature you can reach. On a hit, the target takes 3d10 necrotic damage.
\nForce Potency. When you cast this power using a force slot of 2nd level or higher, the damage increases by 1d10 for each slot level above 1st.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"mpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d10","necrotic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d10"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Sap%20Vitality.webp","effects":[]}
{"_id":"esyvxlavJubgPNmj","name":"Force Trance","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You make a calming gesture, and up to three willing creatures of your choice that you can see within range fall unconscious for the power’s duration. The power ends on a target early if it takes damage or someone uses an action to shake or slap it awake. If a target remains unconscious for the full duration, that target gains the benefit of a short rest, and it can’t be affected by this power again until it finishes a long rest.
\nForce Potency. When you cast this power using a force slot of 4th level or higher, you can target one additional willing creature for each slot level above 3rd.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":3,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1 additional creature"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Trance.webp","effects":[]}
+{"name":"Kinetite","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"Prerequisite: Sustained Lightning
\nA 5-foot-diameter sphere of compressed lightning appears in an unoccupied space of your choice within range and lasts for the duration. Any creature that ends its turn within 5 feet of the sphere must make a Dexterity saving throw. The creature takes 2d6 lightning damage on a failed save, or half as much damage on a successful one.
\nAs a bonus action, you can move the sphere up to 30 feet. If you ram the sphere into a creature, that creature must make a Dexterity saving throw, taking 2d6 kinetic damage on a failed save or half as much damage on a successful one, and the sphere stops moving this turn.
\nWhen you move the sphere, you can direct it over barriers up to 5 feet tall and jump it across pits up to 10 feet wide. The sphere ignites flammable objects not being worn or carried, and it sheds bright light in a 20-foot radius and dim light for an additional 20 feet.
\nForce Potency. When you cast this power with a force slot of 3rd level or higher, both the lightning and kinetic damage increase by 1d6 for each slot level above 2nd.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":5,"width":null,"units":"ft","type":"sphere"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6","lightning"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":2,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"core":{"sourceId":"Item.sOyvjT8ig2Eninf3"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Kinetite.webp","effects":[],"_id":"f01f9EPd9qvVy6QA"}
{"_id":"f41D14g0yIBuXUka","name":"Force Immunity","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"An immobile, faintly shimmering barrier springs into existence around you and remains for the duration. The barrier moves with you. Any force power of 3rd level or lower cast from outside the barrier can’t affect you, even if the power is cast using a higher level force slot. Such a power can target you, but the power has no effect on you. Similarly, the area within the barrier is excluded from the areas affected by such powers.
\nForce Potency. When you cast this power using a force slot of 5th level or higher, the barrier blocks powers of one level higher for each slot level above 4th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1 additional slot"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Immunity.webp","effects":[]}
{"_id":"faTYDaOejYHPfXdl","name":"Earthquake","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Eruption
\nYou create a seismic disturbance at a point on the ground that you can see within range. For the duration, an intense tremor rips through the ground in a 100-foot-radius circle centered on that point and shakes creatures and structures in contact with the ground in that area.
\nThe ground in the area becomes difficult terrain. Each creature on the ground that is concentrating must make a Constitution saving throw. On a failed save, the creature’s concentration is broken.
\nWhen you cast this power and at the end of each turn you spend concentrating on it, each creature on the ground in the area must make a Dexterity saving throw. On a failed save, the creature is knocked prone.
\nThis power can have additional effects depending on the terrain in the area, as determined by the GM.
\nFissures. Fissures open throughout the power’s area at the start of your next turn after you cast the power. A total of 1d6 such fissures open in locations chosen by the GM. Each is 1d10 x 10 feet deep, 10 feet wide, and extends from one edge of the power’s area to the opposite side. A creature standing on a spot where a fissure opens must succeed on a Dexterity saving throw or fall in. A creature that successfully saves moves with the fissure’s edge as it opens.
\nA fissure that opens beneath a structure causes it to automatically collapse (see below).
\nStructures. The tremor deals 50 kinetic damage to any structure in contact with the ground in the area when you cast the power and at the start of each of your turns until the power ends. If a structure drops to 0 hit points, it collapses and potentially damages nearby creatures. A creature within half the distance of a structure’s height must make a Dexterity saving throw. On a failed save, the creature takes 5d6 kinetic damage, is knocked prone, and is buried in the rubble, requiring a DC 20 Strength (Athletics) check as an action to escape. The GM can adjust the DC higher or lower, depending on the nature of the rubble. On a successful save, the creature takes half as much damage and doesn’t fall prone or become buried.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":100,"units":"ft","type":"radius"},"range":{"value":500,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":8,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Earthquake.webp","effects":[]}
{"_id":"fcs6lsi5hMfPOgRb","name":"Resistance","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You touch one willing creature. Once before the power ends, the target can roll a d4 and add the number rolled to one saving throw of its choice. It can roll the die before or after the saving throw. The power then ends.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Resistance.webp","effects":[]}
@@ -184,6 +188,7 @@
{"_id":"qIelaqr9e9Cn0W8O","name":"Calm Emotions","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Sense Emotion
\nYou attempt to suppress strong emotions in a group of people. Each humanoid in a 20-foot-radius sphere centered on a point you choose within range must make a Charisma saving throw a creature can choose to fail this saving throw if it wishes. If a creature fails its saving throw, choose one of the following two effects.
\n\nYou can suppress any effect causing a target to be charmed or frightened. When this power ends, any suppressed effect resumes, provided that its duration has not expired in the meantime. \nYou can make a target indifferent about creatures of your choice that it is hostile toward. This indifference ends if the target is attacked or harmed by a power or if it witnesses any of its friends being harmed. \n \nWhen the power ends, the creature becomes hostile again, unless the GM rules otherwise.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":20,"units":"ft","type":"sphere"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"cha","dc":null,"scaling":"power"},"level":2,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Calm%20Emotions.webp","effects":[]}
{"_id":"qYnIKhpoJpSflVZh","name":"Telekinetic Burst","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Telekinetic Storm
\nA beam of Force energy flashes out from your hand in a 5-foot-wide, 60-foot-long line. Each creature in the line must make a Constitution saving throw. On a failed save, a creature takes 8d6 force damage and is knocked prone. On a successful save, it takes half as much damage and isn’t knocked prone.
\nYou can create a new telekinetic gust as your action on your turn until the power ends.
\nForce Potency. When you cast this power using a force slot of 7th level or higher, the damage increases by 2d6 for each slot level above 6th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":60,"units":"ft","type":"line","width":null},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["8d6","force"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":6,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"2d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Telekinetic%20Burst.webp","effects":[]}
{"_id":"qkBzg8ZIJpglVMvi","name":"Beacon of Hope","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Heroism
\nThis power bestows hope and vitality. Choose any number of creatures within range. For the duration, each target has advantage on Wisdom saving throws and death saving throws, and regains the maximum number of hit points possible from any healing.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"any","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Beacon%20of%20Hope.webp","effects":[]}
+{"name":"Defensive Technique","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"As part of the action used to cast this power, you must make a melee weapon attack against one creature within your reach, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and you ward yourself from it through the Force.
\nIf the target forces you to make a saving throw before the start of your next turn, the target takes an additional 1d6 psychic damage, and you can roll 1d4 and add the number rolled to your saving throw. The power then ends.
\nThe power's damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d6 force damage to the target, and the psychic damage the target takes for forcing you to make a saving throw increases to 2d6. Both damage rolls increase by 1d6 at 11th level and 17th level.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":"you must make a melee weapon attack against one creature within your reach, otherwise the power fails"},"duration":{"value":1,"units":"round"},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.ielR5DiQGGDRsJB5"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Defensive%20Technique.webp","effects":[],"_id":"qv4hHFZKUwg7Ail2"}
{"_id":"qykEFT52bywaQrNO","name":"Force Technique","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"You imbue your weapon with the purifying light of the Force. As part of the action used to cast this power, you must make a melee attack with a weapon against one creature within your weapon’s reach, otherwise the power fails. On a hit, the target suffers the attack’s normal effects, and it becomes wreathed in a glowing barrier of force energy until the start of your next turn. If the target willingly moves before then, it immediately takes 1d8 force damage, and the power ends.
\nThis power’s damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d8 force damage to the target, and the damage the target takes for moving increases to 2d8. Both damage rolls increase by 1d8 at 11th level and 17th level.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"You must make a melee attack with a weapon against one creature within your weapon’s reach"},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"lgt","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"atwill","formula":"1d8"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Force%20Technique.webp","effects":[]}
{"_id":"rxvZoFgkC411tibT","name":"Break","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"You inflict 10 force damage to an object you can see within range that is not being worn or carried, generating an explosion of sound that can be heard up to 100 feet away. Even if the object is not destroyed, small shards of shrapnel fly at creatures within 5 feet of it. Each creature must make a Dexterity saving throw. On a failed save, a creature takes 1d4 kinetic damage.
\nThis power's kinetic damage increases by 1d4 when you reach 5th level (2d4), 11th level (3d4), and 17th level (4d4), and the power's force damage also increases by 10 at each of these levels.
","chat":"","unidentified":""},"source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"width":null,"units":"","type":"object"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rpak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["10","force"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"uni","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.sVK2BXvC9pEX8By5"}},"img":"systems/sw5e/packs/Icons/Force%20Powers/Break.webp","effects":[]}
{"_id":"sFLwKTBxnM6YfboP","name":"Wrack","permission":{"default":0,"00LEX91CfLcf3HGf":3},"type":"power","data":{"description":{"value":"Prerequisite: Plague
\nYou wrack the body of a creature that you can see with a virulent, disease-like condition. The target must make a Constitution saving throw. On a failed save, it takes 14d6 necrotic damage, or half as much damage on a successful save. The damage can’t reduce the target’s hit points below 1. If the target fails the saving throw, its hit point maximum is reduced for 1 hour by an amount equal to the necrotic damage it took. Any effect that removes a disease allows a creature’s hit point maximum to return to normal before that time passes.
\nForce Potency. If you cast this power using a force slot of 7th level or higher, the power deals an extra 2d6 damage for each slot level above 6th.
","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":"Plague"},"duration":{"value":null,"units":"inst"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["14d6","necrotic"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":6,"school":"drk","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"2d6"},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"consume":{"type":"","target":"","amount":null}},"flags":{},"img":"systems/sw5e/packs/Icons/Force%20Powers/Wrack.webp","effects":[]}
diff --git a/packs/packs/techpowers.db b/packs/packs/techpowers.db
index 511b8ffb..aab4a3c4 100644
--- a/packs/packs/techpowers.db
+++ b/packs/packs/techpowers.db
@@ -20,6 +20,7 @@
{"_id":"6iIYMjmyWE2wqgzf","name":"Carbonite","permission":{"default":0},"type":"power","data":{"description":{"value":"You attempt to freeze one creature that you can see within range into carbonite. The creature must make a Constitution saving throw. On a failed save, it is restrained as its flesh begins to harden. On a successful save, the creature isn't affected.
\nA creature restrained by this power must make another Constitution saving throw at the end of each of its turns. If it successfully saves against this power three times, the power ends. If it fails its saves three times, it is turned to stone and subjected to the petrified condition for the duration. The successes and failures don't need to be consecutive; keep track of both until the target collects three of a kind.
\nIf the creature is physically broken while frozen in carbonite, it suffers from similar deformities if it reverts to its original state.
\nIf you maintain your concentration on this power for the entire possible duration, the creature is frozen in carbonite until the effect is removed.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":6,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Carbonite.webp"}
{"_id":"6oJ07fhBJLXJ7zv1","name":"Scramble Interface","permission":{"default":0},"type":"power","data":{"description":{"value":"You choose one droid or construct you can see within range and scramble its ability to differentiate targets. The target must make an Intelligence saving throw. If the construct has the 'Piloted' trait, and has a pilot controlling it that is not incapacitated, it gains a bonus to the saving throw equal to the pilot's Intelligence modifier. On a failed save, the target loses the ability to distinguish friend from foe, regarding all creatures it can see as enemies until the power ends. Each time the target takes damage, it can repeat the saving throw, ending the effect on itself on a success.
\nWhenever the affected creature chooses another creature as a target, it must choose the target at random from among the creatures it can see within range of the attack, power, or other ability it's using. If an enemy provokes an opportunity attack from the affected creature, the creature must make that attack if it is able to.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"int","dc":null,"scaling":"power"},"level":3,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/ScrambleInterface.webp"}
{"_id":"73W8rKPEbN60y7L2","name":"Defibrillate","permission":{"default":0},"type":"power","data":{"description":{"value":"You touch a creature that has died within the last minute and administer a shock to restore it to life. That creature returns to life with 1 hit point. This power can't return to life a creature that has died of old age, nor can it restore any missing body parts. If the creature is lacking body parts or organs integral for its survival�its head, for instance�the power automatically fails. Once this power has restored a creature to life, it cannot benefit from this power again until it finishes a short or long rest.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":4,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Defibrillate.webp"}
+{"name":"Tri-Shot","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"Choose up to three creatures within range, none of whom can be more than 10 feet apart. If you choose three creatures, each target must succeed on a Dexterity saving throw or take 1d4 energy damage. If you choose two creatures, each target takes 1d6 energy damage on a failed save instead. If you choose only one creature, the target takes 1d8 energy damage on a failed save.
\nThis power's damage increases by one die when you reach 5th level (two dice), 11th level (three dice), and 17th level (four dice).
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":3,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.Tjrr6QDVOaLdRHZO"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Tri-Shot.webp","effects":[],"_id":"782klgZPJ4zjZX4E"}
{"_id":"7Twjeo1X2oUP9IZo","name":"Elemental Accelerant","permission":{"default":0},"type":"power","data":{"description":{"value":"Choose one creature you can see and one damage type: acid, cold, fire, lightning, or sonic. The target must make a Constitution saving throw. If it fails, the first time on each turn when it takes damage of the chosen type, it takes an extra 2d6 damage of it. The target also loses resistance to the type until the power ends.
\nOvercharge Tech. You can target one additional creature for each slot level above 4th. The creatures must be within 30 feet of each other when you target them.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":90,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6",""]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":4,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/ElementalAccelerant.webp"}
{"_id":"7khirDTQvs7rtLbW","name":"Copy","permission":{"default":0},"type":"power","data":{"description":{"value":"This power creates a perfect duplicate of any written, drawn, or digital visual, audio or text-based data that you touch onto a datapad or datacard you supply. You can copy up to 10 pages of text or 10 minutes of visual or audio data with one casting of this power. Enhanced documents, such as datacrons, blueprints, or encrypted documents, can't be copied with this power.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"object"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Copy.webp"}
{"_id":"7o2xvsn9AVML11ME","name":"Storming Shot","permission":{"default":0},"type":"power","data":{"description":{"value":"As a part of the action used to cast this power, you must make a ranged weapon attack against one creature within your weapon's range, otherwise the power fails. On a hit, the target suffers the attack's normal effects and becomes shocked until the end of your next turn. When this power hits a target, if there is a creature within 30 feet who is shocked, an arc of lightning courses between the two creatures, dealing 1d6 lightning damage to both of them. If there are multiple other creatures who are shocked, the lightning leaps to the closest creature.
\nThe power's damage increases when you reach higher levels. At 5th level, the effects of both the ranged weapon attack and discharge deal an extra 1d6 lightning damage. Both damage rolls increase by an additional 1d6 at 11th and 17th level.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"other","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6","lightning"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/StormingShot.webp"}
@@ -132,6 +133,7 @@
{"_id":"Zk697eWSqME5gpkF","name":"Condense/Vaporize","permission":{"default":0},"type":"power","data":{"description":{"value":"In an open container, you can create up to 10 gallons of drinkable water. You may also produce a rain that falls within a 30-foot cube and extinguishes open-air flames. You can destroy the same amount of water in an open container, or destroy a 30-foot cube of fog.
\nOvercharge Tech. When you cast this power using a tech slot of 2nd level or higher, the amount of water you can create increases by 10 gallons, or the size of the cube increases by 5 feet, for each slot level above 1st.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"object"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/CondenseVaporize.webp"}
{"_id":"a4nZ05eHgTXZo4TU","name":"Cage","permission":{"default":0},"type":"power","data":{"description":{"value":"An immobile, Invisible, cube-shaped prison composed of energy springs into existence around an area you choose within range. The prison can be a cage or a solid box as you choose.
\nA prison in the shape of a cage can be up to 20 feet on a side and is made from 1/2-inch diameter bars spaced 1/2 inch apart.
\nA prison in the shape of a box can be up to 10 feet on a side, creating a solid barrier that prevents any matter from passing through it and blocking any powers cast into or out of the area.
\nWhen you cast the power, any creature that is completely inside the cage's area is trapped. Creatures only partially within the area, or those too large to fit inside the area, are pushed away from the center of the area until they are completely outside the area.
\nA creature inside the cage can't leave it by unenhanced means. If the creature tries to teleport to leave the cage, it must first make a Charisma saving throw. On a success, the creature can use that power to exit the cage. On a failure, the creature can't exit the cage and wastes the use of the power or effect.
\nThis power can't be dispelled.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":20,"units":"ft","type":"cube"},"range":{"value":100,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":7,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Cage.webp"}
{"_id":"aJqCpoejohXYuJ1J","name":"Sonic Strike","permission":{"default":0},"type":"power","data":{"description":{"value":"As part of the action used to cast this power, you must make a melee weapon attack against one creature within your reach, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and you begin to emanate a disturbing hum. If a hostile creature ends its turn within 5 feet of you before the start of your next turn, it takes 1d4 sonic damage.
\nThis power's damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d8 sonic damage to the target, and the secondary damage increases by 1d4. Both damage rolls increase by 1d8 and 1d4, respectively, at 11th level and 17th level.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d8"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/SonicStrike.webp"}
+{"name":"Aid Droid","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"Choose one droid or construct that you can see within range. The target can immediately use its reaction to regain hit points equal to 1d6 + your techcasting ability modifier.
\nOvercharge Tech. When you cast this power using a tech slot of 2nd level or higher, the healing increases by 1d6 for each slot level above 1st.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":"inst"},"target":{"value":1,"width":null,"units":"","type":"droid"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"heal","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6+@abilities.int.mod","healing"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"core":{"sourceId":"Item.xI9OeBQggss4H0AI"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Aid%20Droid.webp","effects":[],"_id":"aW2gm0t0dNq2YMcm"}
{"_id":"b0T7rxNgDtGx3mwh","name":"Stack the Deck","permission":{"default":0},"type":"power","data":{"description":{"value":"You boost up to three creatures of your choice within range. Whenever a target makes an attack roll or a saving throw before the power ends, the target can roll a d4 and add the number rolled to the attack roll or saving throw.
\nOvercharge Tech. When you cast this power with a tech slot of 2nd level or higher, you can target one additional creature for every two slot levels above 1st. When you cast this power at 3rd level or higher, the die size increases for every two slot levels above 1st (d6 at 3rd level, d8 at 5th level, d10 at 7th level).
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Stackthe%20Deck.webp"}
{"_id":"bp55Q4R0gBpd0FiM","name":"Oil Slick","permission":{"default":0},"type":"power","data":{"description":{"value":"You cover the ground in a 10-foot square within range in oil. For the duration, it is difficult terrain.
\nWhen the oil appears, each creature standing in its area must succeed on a Dexterity saving throw or fall prone. A creature that enters the area or ends its turn there must also succeed on a Dexterity saving throw.
\nThe oil is flammable. Any 5 foot square of the oil exposed to fire burns away in one round. Each creature who enters the fire or starts it turn there must make a Dexterity saving throw, taking 3d6 fire damage on a failed save, or half as much on a successful one. The fire ignites any flammable objects in the area that aren't being worn or carried.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":10,"units":"ft","type":"square"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["3d6","fire"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/OilSlick.webp"}
{"_id":"c17MNNJ8FplU5Txm","name":"Absorb Energy","permission":{"default":0},"type":"power","data":{"description":{"value":"The power captures some of the incoming energy, lessening its effect on you and storing it for your next melee attack. You have resistance to the triggering damage type until the start of your next turn. Also, the first time you hit with a melee attack on your next turn, the target takes an extra 1d6 damage of the triggering type, and the power ends.
\nOvercharge Tech. When you cast this power using a power slot of 2nd level or higher, the extra damage increases by 1d6 for each slot level above 1st.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"reaction","cost":1,"condition":"which you take when you take acid, cold, energy, fire, ion, kinetic, lightning, or sonic damage"},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6",""]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":false},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/AbsorbEnergy.webp","effects":[]}
@@ -190,6 +192,7 @@
{"_id":"to84bBMy9F4zYZ5I","name":"Combustive Shot","permission":{"default":0},"type":"power","data":{"description":{"value":"As part of the action used to cast this power, you must make a ranged weapon attack against one creature within your weapon's range, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and it ignites in flame. At the start of your next turn, the creature takes fire damage equal to your techcasting ability modifier. If the target or a creature within 5 feet of it uses an action to put out the flames, or if some other effect douses the flames, the effect ends.
\nThis power's damage increases when you reach higher levels. At 5th level, the ranged attack deals an extra 1d6 fire damage to the target, and the damage at the start of your next turn increases to 1d4 + your tech casting ability modifier. Both damage rolls increase by 1d6 and 1d4, respectively, at 11th level and 17th level.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"round"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["@abilities.int.mod","fire"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/CombustiveShot.webp"}
{"_id":"uKVIsflQoqXlbejy","name":"Radiation","permission":{"default":0},"type":"power","data":{"description":{"value":"Dim, greenish light spreads within a 30-foot-radius sphere centered on a point you choose within range. The light spreads around corners, and it lasts until the power ends.
\nWhen a creature moves into the power's area for the first time on a turn or starts its turn there, that creature must succeed on a Constitution saving throw or take 4d10 necrotic damage, and it suffers one level of exhaustion and emits a dim, greenish light in a 5-foot radius. This light makes it impossible for the creature to benefit from being invisible. The light and any levels of exhaustion caused by this power go away when the power ends.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":30,"units":"ft","type":"radius"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["4d10","necrotic"]],"versatile":""},"formula":"","save":{"ability":"con","dc":null,"scaling":"power"},"level":4,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Radiation.webp"}
{"_id":"uLB7E27wecdLLbCE","name":"Minor Defibrillation","permission":{"default":0},"type":"power","data":{"description":{"value":"You generate a static charge that can aid or harm a creature you touch. Make a melee tech attack against the target. On a hit, the target takes 1d10 lightning damage. If the target is a living creature that has 0 hit points, it immediately gains one death saving throw success instead of taking damage.
\nThis power's damage increases by 1d10 when you reach 5th level (2d10), 11th level (3d10), and 17th level (4d10).
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"mpak","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10","lightning"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d10"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/MinorDefibrillation.webp"}
+{"name":"Alter Self","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"You alter your form with tech. When you cast the power, choose one of the following options, the effects of which last for the duration of the power. While the power lasts, you can end one option as an action to gain the benefits of a different one.
\nAquatic Adaptation. You adapt your body to an aquatic environment. You can breathe underwater and gain a swimming speed equal to your walking speed.
\nChange Appearance. You transform your appearance. You decide what you look like, including your height, weight, facial features, sound of your voice, hair length, coloration, and distinguishing characteristics, if any. You can make yourself appear as a member of another species (an organic species cannot appear as a droid, or vice versa), though none of your statistics change. You can also alter your vocal cords, enabling you to speak a language you know, but otherwise would be incapable of speaking. You also can’t appear as a creature of a different size than you, and your basic shape stays the same; if you’re bipedal, you can’t use this power to become quadrupedal, for instance. At any time for the duration of the power, you can use your action to change your appearance in this way again.
\nNatural Weapons. You grow claws, fangs, spines, horns, or a different natural weapon of your choice. Your damage die for your unarmed strikes increases by one step (from 1 to d4, d4 to d6, or d6 to d8). Your unarmed strikes are considered enhanced and you have a +1 bonus to attack and damage rolls with them.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.lGXbhhtTzcGCZQ9b"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Alter%20Self.webp","effects":[],"_id":"uuxkihWcbMdubsLx"}
{"_id":"vo6OJqBxuOiyMyxX","name":"Kolto Cloud","permission":{"default":0},"type":"power","data":{"description":{"value":"As you expel kolto, up to six creatures of your choice that you can see within range regain hit points equal to 1d4 + your techcasting ability modifier. This power has no effect on droids or constructs.
\nOvercharge Tech. When you cast this power using a tech slot of 4th level or higher, the healing increases by 1d4 for each slot level above 3rd.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":6,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"heal","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @abilities.int.mod","healing"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"level","formula":"1d4"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/KoltoCloud.webp"}
{"_id":"w8aWJGuIPq2kp4Qj","name":"Delayed Explosion","permission":{"default":0},"type":"power","data":{"description":{"value":"You create a delayed explosion at a point within range. When the power ends, either because your concentration is broken or because you decide to end it, the explosion occurs. Each creature in a 20-foot-radius sphere centered on that point must make a Dexterity saving throw. A creature takes fire damage equal to the total accumulated damage on a failed save, or half as much damage on a successful one.
\nThe power's base damage is 12d6. If at the end of your turn the explosion has not yet occurred, the damage increases by 1d6.
\nIf the explosion is touched before the interval has expired, the creature touching it must make a Dexterity saving throw. On a failed save, the power ends immediately, causing the explosion.
\nThe fire spreads around corners. It ignites flammable objects in the area that aren't being worn or carried.
\nOvercharge Tech. When you cast this power using a tech slot of 8th level or higher, the base damage increases by 1d6 for each slot level above 7th.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":20,"units":"ft","type":"radius"},"range":{"value":150,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"save","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["12d6",""]],"versatile":"1d6"},"formula":"","save":{"ability":"dex","dc":null,"scaling":"power"},"level":7,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"level","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/DelayedExplosion.webp"}
{"_id":"wtSlQwtA4N2ewADb","name":"Preparedness","permission":{"default":0},"type":"power","data":{"description":{"value":"You touch a willing creature. For the duration, the target can add 1d8 to its initiative rolls.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":1,"condition":""},"duration":{"value":8,"units":"hour"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"touch"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":1,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Preparedness.webp"}
@@ -197,6 +200,7 @@
{"_id":"xW2LoI7JTHMqPcuC","name":"Mobile Lights","permission":{"default":0},"type":"power","data":{"description":{"value":"You create up to four orbs of light within range that hover in the air for the duration. You can also combine the four lights into one glowing vaguely humanoid form of Medium size. Whichever form you choose, each light sheds dim light in a 10-foot radius.
\nAs a bonus action on your turn, you can move the lights up to 60 feet to a new spot within range. A light must be within 20 feet of another light created by this power, and a light winks out if it exceeds the power's range.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"minute"},"target":{"value":null,"units":"","type":"self"},"range":{"value":120,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/MobileLights.webp"}
{"_id":"xXPplfLNPoXvhfjp","name":"Wire Line","permission":{"default":0},"type":"power","data":{"description":{"value":"You launch a grappling wire toward a creature you can see within range. Make a melee tech attack against the target. If the attack hits, the creature takes 1d6 kinetic damage, and if the creature is Large or smaller, you pull the creature up to 10 feet closer to you.
\nThis power's damage increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"mpak","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d6"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/WireLine.webp"}
{"_id":"xeBPmKsGTsXqEszc","name":"Greater Analyze","permission":{"default":0},"type":"power","data":{"description":{"value":"Name or describe a person, place, or object. This power gives you a summary of significant lore about it. If the thing you named isn't known outside of one planetary system, you gain no information. The more information you already have, the more detailed the information you receive is.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"minute","cost":10,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":5,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/GreaterAnalyze.webp"}
+{"name":"Mutate/Augment","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"power","data":{"description":{"value":"You alter your body with tech, temporarily gaining new properties which last for the duration. You can select three of the following properties. You cannot select a property more than once, unless that property says otherwise.
\n\nYour body becomes more flexible. You have advantage on ability checks and saving throws against effects that would grapple or restrain you, and your movement is unaffected by difficult terrain or squeezing. \nYou grow one additional appendage. This appendage serves as an arm and a hand, though it can take the shape of a limb, tentacle, or similar appendage. You can select this property twice to grow a maximum number of two appendages. You can only gain the benefit of items held by two of your arms at any given time, and once per round you can switch which arms you are benefiting from (no action required). \nYou sprout antennae that give you tremorsense out to a range of 30 feet. \nYou extend the length of your limbs. When you make a melee attack or attempt to grapple, shove, or trip a creature on your turn, your reach for it is 5 feet greater than normal. \nYou gain a burrowing speed equal to your walking speed. \nYour flesh hardens. Your AC can't be less than 16, regardless of what kind of armor you're wearing. \nYou can gain the benefits of any one option of the alter self power. You can select this property multiple times to gain the benefits of multiple options of the alter self power, choosing a different option each time. \nYou gain the effect of the magnetic hold power, but you can affix to and move along any surface, instead of only metallic surfaces. You have a climbing speed equal to your walking speed. \nYou can gain any one effect of the force enlightenment power. \n \nOvercharge Tech. When you cast this power using a tech slot of 4th level or higher, you can select an additional property for each slot level above 3rd.
","chat":"","unidentified":""},"requirements":"","source":"EC","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":10,"units":"minute"},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":"0","max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":3,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":true},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"prepared","prepared":true},"scaling":{"mode":"none","formula":""}},"flags":{"core":{"sourceId":"Item.pY0NUf5jwDrhhiz9"}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/Mutate-Augment.webp","effects":[],"_id":"xoeFnjZ62vkY6ygA"}
{"_id":"xs0igvxRAzGwZqik","name":"Venomous Strike","permission":{"default":0},"type":"power","data":{"description":{"value":"As part of the action used to cast this power, you must make a melee weapon attack against one creature within your reach, otherwise the power fails. On a hit, the target suffers the attack's normal effects, and if you were hidden from it, it takes an additional 1d4 poison damage.
\nThis power's damage increases when you reach higher levels. At 5th level, the melee attack deals an extra 1d8 poison damage to the target, and the damage the target takes when you are hidden from it increases to 2d4. Both damage rolls increase by 1d8 and 1d4, respectively, at 11th level and 17th level.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":"spec"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"util","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4","poison"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":0,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"atwill","formula":"1d8"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/VenomousStrike.webp"}
{"_id":"xxrRddwkMSsJbPs0","name":"Kolto Infusion","permission":{"default":0},"type":"power","data":{"description":{"value":"Choose a creature that you can see within range. A surge of kolto energy washes over the creature, causing it to regain 70 hit points. This power also ends blindness, deafness, and any diseases affecting the target. This power has no effect on droids or constructs.
\nOvercharge Tech. When you cast this power using a tech slot of 7th level or higher, the amount of healing increases by 10 for each slot level above 6th.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"heal","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[["+70","healing"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":6,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"level","formula":"10"}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/KoltoInfusion.webp"}
{"_id":"yBnreezRaiZpukCB","name":"Detect Invisibility","permission":{"default":0},"type":"power","data":{"description":{"value":"For the duration, you see invisible creatures and objects as if they were visible.
\n","chat":"","unidentified":""},"source":"PHB","activation":{"type":"action","cost":1,"condition":""},"duration":{"value":1,"units":"hour"},"target":{"value":0,"units":"","type":""},"range":{"value":null,"long":null,"units":"self"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"int","actionType":"","attackBonus":null,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"level":2,"school":"tec","components":{"value":"","vocal":false,"somatic":false,"material":false,"ritual":false,"concentration":false},"materials":{"value":"","consumed":false,"cost":0,"supply":0},"preparation":{"mode":"","prepared":false},"scaling":{"mode":"none","formula":""}},"flags":{"exportSource":{"world":"sw5e","system":"sw5e","coreVersion":"0.6.6","systemVersion":0.98}},"img":"systems/sw5e/packs/Icons/Tech%20Powers/DetectInvisibility.webp"}
diff --git a/packs/packs/weapons.db b/packs/packs/weapons.db
index d27d9503..39e87327 100644
--- a/packs/packs/weapons.db
+++ b/packs/packs/weapons.db
@@ -1,27 +1,27 @@
{"_id":"0ZRWgHRykApmHmoG","name":"Heavy Shotgun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Reload 12, Strength 13
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Shotgun.webp","effects":[]}
{"_id":"1wmSlQ3a91lL9c3H","name":"Slugpistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 8, Reload 16, Strength 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Slugpistol.webp","effects":[]}
-{"_id":"1zm4z1AKDhzXC2ow","name":"Jagged Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":5,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8+@mod","kinetic"]],"versatile":"1d10+@mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":true,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7187501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"1zm4z1AKDhzXC2ow","name":"Jagged Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":5,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8+@mod","kinetic"]],"versatile":"1d10+@mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":true,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7187501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Jagged%20Vibroblade.webp","effects":[]}
{"_id":"2RG5RZxqJ2q7jziD","name":"Switch carbine","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 60/240), Reload 12, Switch (1d4 acid)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":4600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"1d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799904,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Switch%20Carbine.webp","effects":[]}
{"_id":"2ylNmxKfD5IdGre8","name":"Shatter pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 40/160), Silent, Light, Reload 20
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":12},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899806,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Shatter%20Pistol.webp","effects":[]}
{"_id":"3MBvpQfxedbIW6eH","name":"Vibrospear","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":120,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":"1d8 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrospear.webp","effects":[]}
{"_id":"3u5NHZJFld4ayeEI","name":"Lightdagger","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Lightdagger.webp","effects":[]}
{"_id":"4q1qOqW9eYyAmn8l","name":"Vibrodart","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Due to their diminutive size, vibrodarts make ineffective melee weapons. Melee attack rolls made with them are made at disadvantage.
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":5,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrodart.webp","effects":[]}
{"_id":"50y7jToHJOvsO1Yr","name":"Repeating Blaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Rapid 4, Reload 8, Strength 13
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":18,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Repeater.webp","effects":[]}
-{"_id":"5RF22K1v5Pra1uwf","name":"Chained dagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disarming, Finesse, Reach, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":850,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6950001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"5vPxd3p7M2m9j07G","name":"BKG","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 120/480), Auto, Burst 2, Disintegrate 13, Reload 2, Strength 19, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":42,"price":9000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":10,"width":null,"units":"","type":"cube"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":240},"ability":"dex","actionType":"save","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4 + @mod","fire"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":300001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"62kO29wbh9pYXwb6","name":"Vibroknife","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Finesse, Light, Piercing 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":true,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899995,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"5RF22K1v5Pra1uwf","name":"Chained Dagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disarming, Finesse, Reach, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":850,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6950001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Chained%20Dagger.webp","effects":[]}
+{"_id":"5vPxd3p7M2m9j07G","name":"BKG","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 120/480), Auto, Burst 2, Disintegrate 13, Reload 2, Strength 19, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":42,"price":9000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":10,"width":null,"units":"","type":"cube"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":240},"ability":"dex","actionType":"save","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4 + @mod","fire"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":300001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/BKG.webp","effects":[]}
+{"_id":"62kO29wbh9pYXwb6","name":"Vibroknife","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Finesse, Light, Piercing 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":true,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899995,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroknife.webp","effects":[]}
{"_id":"6VZqOHM0oqK6GiQ9","name":"Needler","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 10, Reload 20
The needler includes a specialized compartment for poison. One dose of poison, when installed in this compartment, retains its potency for 1 hour before drying. One dose of poison is effective for the next 10 shots fired by the weapon.
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":275,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Needler.webp","effects":[]}
-{"_id":"6nqChLn3T4yM9eyV","name":"Vibroglaive","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":13,"price":1250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10+mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7086720,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"6nqChLn3T4yM9eyV","name":"Vibroglaive","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":13,"price":1250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10+mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7086720,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroglaive.webp","effects":[]}
{"_id":"7BNvGSn4OCqYOXRW","name":"Doubleshoto","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Double (1d6 Energy)
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Doubleshoto.webp","effects":[]}
-{"_id":"7TXR7ex23HIFCNOm","name":"Vibrobuster","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":7777,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":true,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199806,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"7TXR7ex23HIFCNOm","name":"Vibrobuster","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":7777,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":true,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199806,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrobuster.webp","effects":[]}
{"_id":"7Tv59onLvYZHkoBo","name":"Wristsaber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Finesse, Fixed, Light, Luminous
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7081251,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Wristsaber.webp","effects":[]}
{"_id":"835xibigP1dU8CTe","name":"Doubleblade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Double (1d6 Kinetic)
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":625,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Doubleblade.webp","effects":[]}
{"_id":"9luXJi8YnsiLIku5","name":"Tranquilizer Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 4
The tranquilizer rifle includes a specialized compartment for poison. One dose of poison, when installed in this compartment, retains its potency for 1 hour before drying. One dose of poison is effective for the next 4 shots fired by the weapon.
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":10,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Tranquilizer%20Rifle.webp","effects":[]}
{"_id":"AgOdU4CCgzkHE7k3","name":"Claymore saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 13, Luminous, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":9,"price":2100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6875001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Claymore%20Saber.webp","effects":[]}
{"_id":"As8MEk6gS194cszO","name":"Lightsaber Pike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Dexterity 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightsaber%20Pike.webp","effects":[]}
-{"_id":"BkJeU3bFIAausZ0R","name":"Warsword","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 13, Two-handed, Vicious 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7062501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"BkJeU3bFIAausZ0R","name":"Warsword","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 13, Two-handed, Vicious 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7062501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Warsword.webp","effects":[]}
{"_id":"CZSl9ojBFp2P7NTo","name":"Hidden Blade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Hidden%20Blade.webp","effects":[]}
-{"_id":"CySLyAoF97hQ5OQE","name":"Shock Whip","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":3,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7198439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"CySLyAoF97hQ5OQE","name":"Shock Whip","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":3,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7198439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Shock%20Whip.webp","effects":[]}
{"_id":"DNzVCv79gSDnvu29","name":"Nightstinger rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 120/480), Silent, Reload 2, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Nightstinger%20Rifle.webp","effects":[]}
{"_id":"ERnfcnNUZKTaEJXK","name":"Lightaxe","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Heavy, Luminous, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":1800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6898439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightaxe.webp","effects":[]}
{"_id":"EVfeatRtUl44AxUD","name":"Energy bow","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 80/320), Mighty, Reload 12, Silent, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":true,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6893751,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Energy%20Bow.webp","effects":[]}
@@ -30,7 +30,7 @@
{"_id":"FqG0kEsg92JQ5qte","name":"Blaster Pistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 16
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":15},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Blaster%20Pistol.webp","effects":[]}
{"_id":"GHm4Ewtx59eisABa","name":"Sith saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 11, Hidden, Keen 1, Luminous, Versatile (1d10)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":2400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6587501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Sith%20Saber.webp","effects":[]}
{"_id":"GajCi06DTH9AWoNB","name":"Retrosaber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"The retrosaber is an ancient type of lightweapon that requires a power cell to function. Once four attacks have been made with a retrosaber, a character must replace the power cell using an action or bonus action (the character’s choice). You must have one free hand to replace the power cell.
\nKeen 1, Luminous, Special, Vicious 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":8,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6687501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Retrosaber.webp","effects":[]}
-{"_id":"GueNpIQQ1PQVT173","name":"Unarmed Strike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"natural","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"GueNpIQQ1PQVT173","name":"Unarmed Strike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"natural","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Unarmed%20Strike.png","effects":[]}
{"_id":"Gxo8kRr0dQje7a9y","name":"Heavy Repeater","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Rapid 2, Reload 8, Strength 15
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Repeater.webp","effects":[]}
{"_id":"HFGuc5KM21MTRpbn","name":"Blaster Carbine","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 16
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":8,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":15},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Blaster%20Carbine.webp","effects":[]}
{"_id":"HOG6cMpPNlpLWWOz","name":"Chaingun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 6, Reload 12, Strength 15
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":42,"price":1500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Chaingun.webp","effects":[]}
@@ -43,7 +43,7 @@
{"_id":"JP2mZAKluouY2JMR","name":"Disruptor pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 60/240), Disintegrate 13, Reload 16
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":4,"price":4000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":15},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","acid"]],"versatile":""},"formula":"","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6887501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Disruptor%20Pistol.webp","effects":[]}
{"_id":"JYZF9unlf2YrPous","name":"Bolt-thrower","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 100/400), reload 2, silent, strength 11, two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":14,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7300001,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bolt%20Thrower.webp","effects":[]}
{"_id":"Jb5Ms4NFiQbB6JNS","name":"Heavy Pistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 12, Strength 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Pistol.webp","effects":[]}
-{"_id":"JivFEWysm82fCFCO","name":"Sonic rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 100/400), Reload 12, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999611,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"JivFEWysm82fCFCO","name":"Sonic Rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 100/400), Reload 12, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Sonic%20Rifle.webp","effects":[]}
{"_id":"K1d8enmp2viW5zG5","name":"Wristblaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 12
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":7200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Wrist%20Blaster.webp","effects":[]}
{"_id":"KGopmgxTmVLnRXF7","name":"Scattergun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Reload 4
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":80,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Scattergun.webp","effects":[]}
{"_id":"KbJhaEvG7rdvtczl","name":"Ion Pistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 16
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d3 + @mod","ion"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Ion%20Pistol.webp","effects":[]}
@@ -52,8 +52,8 @@
{"_id":"LQEPGNraFXSViaWF","name":"Greatsaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Dexterity 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Greatsaber.webp","effects":[]}
{"_id":"LYOgciOEThVXogkW","name":"Guard shoto","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Finesse, Light, Luminous
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":1350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6450001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Guard%20Shoto.webp","effects":[]}
{"_id":"LfBkJ40l2SEWiRvE","name":"Vibrodagger","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":50,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrodagger.webp","effects":[]}
-{"_id":"LyUddEATg6hhVzup","name":"Sonic pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 40/160), Reload 12
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6698439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"MKysOTuoqYnSc7Jj","name":"Riot Baton","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7193751,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"LyUddEATg6hhVzup","name":"Sonic pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 40/160), Reload 12
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","sonic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6698439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Sonic%20Pistol.webp","effects":[]}
+{"_id":"MKysOTuoqYnSc7Jj","name":"Riot Baton","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7193751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Riot%20Baton.webp","effects":[]}
{"_id":"MPdtUMvLZMeS0C0A","name":"Vibrowhip","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":150,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":7000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrowhip.webp","effects":[]}
{"_id":"MQHnsYyAKXByZ0Gc","name":"IWS (Antiarmor)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"\n
The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.
\n
Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.
\n
Blaster. While in this mode, the weapon uses traditional power cells.
\n
Sniper. While in this mode, the weapon uses traditional power cells.
\n
\nAntiarmor: Special, Ammunition (range 60/240), reload 1, special
\nBlaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\nSniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
\nSpecial, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]}
{"_id":"MYRL4RqwpkvpGEF7","name":"ARC caster","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Rapid 2, Reload 4, Special, Strength 11, Two-handed
\nWhen you score a critical hit with this weapon, a creature becomes shocked until the end of its next turn.
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":2400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","lightning"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":100001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/ARC%20Caster.webp","effects":[{"_id":"xGXO5cLr25ibPpu5","flags":{"dae":{"stackable":false,"specialDuration":[],"macroRepeat":"none","transfer":false}},"changes":[{"key":"","value":"0","mode":2,"priority":0}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Martial%20Blasters/ARC%20Caster.webp","label":"Brutal","tint":"","transfer":false}]}
@@ -62,88 +62,88 @@
{"_id":"NpBjZzBSs1Ob34wo","name":"Chakram","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Chakram.webp","effects":[]}
{"_id":"NwH4qn7dnL70hCDK","name":"Buster Saber","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod + (ceil(@abilities.str.mod/2))","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7600001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Buster%20Saber.webp","effects":[]}
{"_id":"Nwyshcc6h8hrwD0b","name":"Hunting Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 2, Strength 13
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":150,"long":600,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Hunting%20Rifle.webp","effects":[]}
-{"_id":"OpfE2GuNbGjrKaMn","name":"Crossguard saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Dexterity 13, Heavy, Luminous, Versatile (1d10)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":950,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6987501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"OpfE2GuNbGjrKaMn","name":"Crossguard saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Dexterity 13, Heavy, Luminous, Versatile (1d10)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":950,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6987501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Crossguard%20Saber.webp","effects":[]}
{"_id":"OvrgbStdTiaZJOkp","name":"Vibropike","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Dexterity 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6500001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibropike.webp","effects":[]}
-{"_id":"P7kHmua3GwxvDubc","name":"Switch pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 40/160), Light, Reload 12, Switch (1d4 fire)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":4,"price":3100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"1d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899904,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"P7kHmua3GwxvDubc","name":"Switch Pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 40/160), Light, Reload 12, Switch (1d4 fire)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":4,"price":3100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"1d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899904,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Pistol.webp","effects":[]}
{"_id":"PVE0NHcsMVC3J2nU","name":"Light Repeater","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 8, Reload 16
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Light%20Repeater.webp","effects":[]}
{"_id":"RLMIkAhL6BzW5W0O","name":"Techaxe","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":75,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Techaxe.webp","effects":[]}
-{"_id":"RPWhectchRTOaP5O","name":"Hooked Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":6,"price":700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7175001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"RPWhectchRTOaP5O","name":"Hooked Vibroblade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":6,"price":700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7175001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Hooked%20Vibroblade.webp","effects":[]}
{"_id":"Rcb4rtOCKFfbykMQ","name":"Cross-saber","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":13,"price":5500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod + (ceil(@abilities.str.mod/2))","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7078126,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Cross-saber.webp","effects":[]}
{"_id":"SBFDSFPd249rnk4s","name":"Techblade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Techblade.webp","effects":[]}
{"_id":"SHP3va9YHWE2LRcD","name":"Heavy Slugpistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 2, Reload 8, Strength 13
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Slugpistol.webp","effects":[]}
-{"_id":"SuHWBtXDAMPQFP39","name":"Electroprod","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","lightning"]],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6050001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"SuHWBtXDAMPQFP39","name":"Electroprod","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","lightning"]],"versatile":""},"formula":"1d4","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6050001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electroprod.webp","effects":[]}
{"_id":"SxhcmZJeFmzuqNLa","name":"Saberaxe","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Brutal 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":5,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6693751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saberaxe.webp","effects":[]}
-{"_id":"U6mqCK7zhOCdayoV","name":"Torpedo Launcher","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rather than traditional power cells, the torpedo launcher fires specialized projectiles in the form of torpedoes. Torpedo launchers have advantage on attack rolls against Gargantuan creatures and disadvantage on attack rolls again Large and smaller creatures. Unlike other weapons, the torpedo launcher can only be loaded using an action, and you don’t add your Dexterity modifier to damage rolls you make with it.
\nBefore firing the torpedo launcher, you must first use your action to deploy it. While deployed, your speed is reduced by half. You can collapse the torpedo launcher as a bonus action.
","chat":"","unidentified":""},"source":"","quantity":1,"weight":25,"price":10000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":1200,"long":3600,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199220,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"U6mqCK7zhOCdayoV","name":"Torpedo Launcher","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rather than traditional power cells, the torpedo launcher fires specialized projectiles in the form of torpedoes. Torpedo launchers have advantage on attack rolls against Gargantuan creatures and disadvantage on attack rolls again Large and smaller creatures. Unlike other weapons, the torpedo launcher can only be loaded using an action, and you don’t add your Dexterity modifier to damage rolls you make with it.
\nBefore firing the torpedo launcher, you must first use your action to deploy it. While deployed, your speed is reduced by half. You can collapse the torpedo launcher as a bonus action.
","chat":"","unidentified":""},"source":"","quantity":1,"weight":25,"price":10000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":1200,"long":3600,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Torpedo%20Launcher.webp","effects":[]}
{"_id":"UAdGYMpSYZO9PgDN","name":"Doublesaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Double (1d8 Energy)
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Doublesaber.webp","effects":[]}
{"_id":"UB1iH8foflZXxMSM","name":"Net","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"A Large or smaller creature hit by a net is restrained until it is freed. A net has no effect on formless or Huge or larger creatures. A creature can use its action to make a DC 13 Strength check, freeing itself or another creature within its reach on a success. The net has an AC of 10, 5 hit points, and immunity to all damage not dealt by melee weapons. Destroying the net frees the creature without harming it and immediately ends the net's effects. While a creature is restrained by a net, you can make no further attacks with it.
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":15,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"str","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Net.webp","effects":[]}
{"_id":"UtC6M3dEyUUh8IHj","name":"Railgun","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 150/600), Piercing 1, Reload 1, Strength 15, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":24,"price":6300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":150,"long":600,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":240},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":true,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6550001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Rail%20Gun.webp","effects":[]}
{"_id":"UxEuTaODfHttK1Bn","name":"Vibrosword","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Dexterity 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrosword.webp","effects":[]}
-{"_id":"VSPlcHGwB9dXwwKo","name":"Vibrotonfa","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Finesse, Light
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mad","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999953,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"Vea8C1Pacl9ZEPhI","name":"Vibrobattleaxe","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(@abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199611,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"VSPlcHGwB9dXwwKo","name":"Vibrotonfa","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Finesse, Light
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1000,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mad","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999953,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrotonfa.webp","effects":[]}
+{"_id":"Vea8C1Pacl9ZEPhI","name":"Vibrobattleaxe","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12+@mod+(ceil(@abilities.str.mod/2))","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7199611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrobattleaxe.webp","effects":[]}
{"_id":"Vx3poKOlfgF1QAl4","name":"Lightsaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":"1d8 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Lightsaber.webp","effects":[]}
{"_id":"WykXGVs1mPUlapA8","name":"Light Ring","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":3,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Light%20Ring.webp","effects":[]}
{"_id":"XEEWBisdZKuTWrwS","name":"Heavy Bowcaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Reload 8, Strength 15
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":26,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Heavy%20Bowcaster.webp","effects":[]}
-{"_id":"XF7LLzqx4ayiY8Gf","name":"Shoulder Cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Mounted by the shoulder slot, a shoulder cannon does not require a free hand to use. Additionally, you have advantage on Strength ability checks and saving throws to avoid being disarmed of this weapon.
\nAmmunition (range 60/240), Autotarget (15, +2), Burst 4, Reload 4, Special
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":9,"price":3200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + 2","energy"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":12,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6696876,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[{"_id":"KCrgDQZ4Fs0XYwyf","flags":{"dae":{"stackable":false,"specialDuration":[],"macroRepeat":"none","transfer":false}},"changes":[{"key":"data.bonuses.rwak.attack","value":"0","mode":2,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"icons/svg/mystery-man.svg","label":"Autotarget","tint":"","transfer":false}]}
-{"_id":"Yi36jcAGqVrealrD","name":"Mancatcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"When you would make a Strength (Athletics) check to attempt to grapple a creature while wielding a weapon with the grappling property, you can instead make a melee weapon attack with it. If the attack hits, the creature becomes grappled by you, and it takes damage equal to your Strength modifier of the same type as the weapon’s damage.
\nReach, Special, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6798439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"Yr8n7ZT1OmoxDmhl","name":"Vibroshield","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Fixed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999904,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"XF7LLzqx4ayiY8Gf","name":"Shoulder Cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Mounted by the shoulder slot, a shoulder cannon does not require a free hand to use. Additionally, you have advantage on Strength ability checks and saving throws to avoid being disarmed of this weapon.
\nAmmunition (range 60/240), Autotarget (15, +2), Burst 4, Reload 4, Special
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":9,"price":3200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + 2","energy"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":12,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6696876,"flags":{},"img":"systems/sw5e/packs/Icons/","effects":[{"_id":"KCrgDQZ4Fs0XYwyf","flags":{"dae":{"stackable":false,"specialDuration":[],"macroRepeat":"none","transfer":false}},"changes":[{"key":"data.bonuses.rwak.attack","value":"0","mode":2,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Martial%20Blasters/Shoulder%20Cannon.webp","label":"Autotarget","tint":"","transfer":false}]}
+{"_id":"Yi36jcAGqVrealrD","name":"Mancatcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"When you would make a Strength (Athletics) check to attempt to grapple a creature while wielding a weapon with the grappling property, you can instead make a melee weapon attack with it. If the attack hits, the creature becomes grappled by you, and it takes damage equal to your Strength modifier of the same type as the weapon’s damage.
\nReach, Special, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6798439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Mancatcher.webp","effects":[]}
+{"_id":"Yr8n7ZT1OmoxDmhl","name":"Vibroshield","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Fixed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999904,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroshield.webp","effects":[]}
{"_id":"ZGERUZSkFxFxSid4","name":"Cycler Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 2, Reload 8
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":10,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":1200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Cycler%20Rifle.webp","effects":[]}
{"_id":"ZHImgyS79q62mnk3","name":"Wrist Launcher","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 1
Rather than traditional power cells, the wrist launcher fires specialized projectiles in the form of darts, small missiles, or specialized canisters.
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":7100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Wrist%20Launcher.webp","effects":[]}
-{"_id":"Zjy957ONj3PyVQm4","name":"Wristblade","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Fixed, Light
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7087501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"Zjy957ONj3PyVQm4","name":"Wristblade","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Fixed, Light
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7087501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Wristblade.webp","effects":[]}
{"_id":"a9lLCncsribwUR2g","name":"IWS (Sniper)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"\n
The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.
\n
Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.
\n
Blaster. While in this mode, the weapon uses traditional power cells.
\n
Sniper. While in this mode, the weapon uses traditional power cells.
\n
\nAntiarmor: Special, Ammunition (range 60/240), reload 1, special
\nBlaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\nSniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
\nSpecial, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]}
{"_id":"aVdtguY4dQuv2xhf","name":"Lightglaive","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 13, Luminous, Reach, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":1900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightglaive.webp","effects":[]}
{"_id":"ahWWUPiXhooCgdKf","name":"Saberwhip","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saberwhip.webp","effects":[]}
{"_id":"ankoAzbua0rlIBnh","name":"Subrepeater","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 8, Reload 16
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5100001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Subrepeater.webp","effects":[]}
{"_id":"b0WBwJRTPPJpnHii","name":"Vibroblade","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":150,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5900001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroblade.webp","effects":[]}
-{"_id":"cnqFdX8ihPdIgSRt","name":"Electrobaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Finesse, Light, Shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":3350001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"cnqFdX8ihPdIgSRt","name":"Electrobaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Finesse, Light, Shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":650,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":3350001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrobaton.webp","effects":[]}
{"_id":"cpuAeXISuY1IRiw9","name":"Lightfist","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Disguised, Fixed, Light, Luminous
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Lightfist.webp","effects":[]}
-{"_id":"ctSLHcbXiV8P0PeG","name":"Riot Shocker","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7196876,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"e4WTWeABUAmn6GZc","name":"Vibrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Heavy, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899989,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"ctSLHcbXiV8P0PeG","name":"Riot Shocker","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":4,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7196876,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Riot%20Shocker.webp","effects":[]}
+{"_id":"e4WTWeABUAmn6GZc","name":"Vibrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Heavy, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899989,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrohammer.webp","effects":[]}
{"_id":"eOf2AzadJEdKZ8NK","name":"Vibrolance","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"You have disadvantage when you use a vibrolance to attack a target within 5 feet of you. Also, a lance requires two hands to wield when you aren't mounted.
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibrolance.webp","effects":[]}
-{"_id":"eT1EM9VkO6zJ4QYZ","name":"Switch sniper","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 120/480), Reload 2, Strength 13, Switch (1d10 cold), Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":8250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"1d10 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6699220,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"eT1EM9VkO6zJ4QYZ","name":"Switch Sniper","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 120/480), Reload 2, Strength 13, Switch (1d10 cold), Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":8250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"1d10 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6699220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Sniper.webp","effects":[]}
{"_id":"fA347pHZ31UJT39y","name":"Vibrocutter","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 11, Heavy, Vicious 1
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":5,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mos","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":true,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799977,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrocutter.webp","effects":[]}
{"_id":"fHvjrYSxapHKDmXt","name":"Vibroknuckler","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":60,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibroknuckler.webp","effects":[]}
{"_id":"fOS7j0XoMXAkb2Iy","name":"Rocket launcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Rather than traditional power cells, the rocket launcher fires specialized projectiles in the form of rockets. When firing a rocket at long range, or if you don’t meet the rocket launcher’s strength requirement, creatures within the radius of the rocket’s explosion have advantage on the saving throw.
\nAmmunition (range 100/400), Reload 1, Special, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":20,"price":2400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799611,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Rocket%20Launcher.webp","effects":[]}
-{"_id":"fV8miB2X8CfTaFkM","name":"Vapor projector","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"The vapor projector does not make attack rolls. Rather than traditional power cells, the vapor projector uses specialized projector tanks, which, when fired, spray an area with the contents of the tank. Projector tanks require your target to make a saving throw to resist the tank’s effects. It can have different ammunition types loaded simultaneously, and you can choose which ammunition you’re using as you fire it (no action required). If you don’t meet the vapor projector’s strength requirement, creatures have advantage on their saving throws. If you lack proficiency in the vapor projector, you must roll the damage dice twice and take the lesser total.
\nAmmunition (range special), Reload 5, Special, Strength 11, Two-handed
","chat":"","unidentified":""},"source":"","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"space"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899953,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"fV8miB2X8CfTaFkM","name":"Vapor Projector","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"The vapor projector does not make attack rolls. Rather than traditional power cells, the vapor projector uses specialized projector tanks, which, when fired, spray an area with the contents of the tank. Projector tanks require your target to make a saving throw to resist the tank’s effects. It can have different ammunition types loaded simultaneously, and you can choose which ammunition you’re using as you fire it (no action required). If you don’t meet the vapor projector’s strength requirement, creatures have advantage on their saving throws. If you lack proficiency in the vapor projector, you must roll the damage dice twice and take the lesser total.
\nAmmunition (range special), Reload 5, Special, Strength 11, Two-handed
","chat":"","unidentified":""},"source":"","quantity":1,"weight":0,"price":0,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"space"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899953,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Vapor%20Projector.webp","effects":[]}
{"_id":"fzFc5SKhOn55embe","name":"Saber Gauntlet","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":2,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8+@mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7084376,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saber%20Guantlet.webp","effects":[]}
{"_id":"gise2mIw1wbTUtlY","name":"Blaster Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 12
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":11,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Blaster%20Rifle.webp","effects":[]}
{"_id":"gxIw7zHI4R5eef9Z","name":"Revolver","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Rapid 6, Reload 6, Strength 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Revolver.webp","effects":[]}
-{"_id":"ioLa4LwWiQTOmgq2","name":"Bolas","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"A Large or smaller creature hit by a bolas is restrained until it is freed. A bolas has no effect on formless or Huge or larger creatures. A creature can use its action to make a DC 13 Dexterity check, freeing itself or another creature within its reach on a success. The bolas has an AC of 10, 5 hit points, and immunity to all damage not dealt by melee weapons. Destroying the bolas frees the creature without harming it and immediately ends the bolas’s effects. While a creature is restrained by a bolas, you can make no further attacks with it.
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":70,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6850001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"k0jg2u21xDQ0zDzf","name":"Vibroclaymore","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":14,"price":1050,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7085939,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"ioLa4LwWiQTOmgq2","name":"Bolas","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"A Large or smaller creature hit by a bolas is restrained until it is freed. A bolas has no effect on formless or Huge or larger creatures. A creature can use its action to make a DC 13 Dexterity check, freeing itself or another creature within its reach on a success. The bolas has an AC of 10, 5 hit points, and immunity to all damage not dealt by melee weapons. Destroying the bolas frees the creature without harming it and immediately ends the bolas’s effects. While a creature is restrained by a bolas, you can make no further attacks with it.
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":70,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":20,"long":60,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6850001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Bolas.webp","effects":[]}
+{"_id":"k0jg2u21xDQ0zDzf","name":"Vibroclaymore","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":14,"price":1050,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["3d4+@mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7085939,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroclaymore.webp","effects":[]}
{"_id":"k5ippYtz0p4PjbOW","name":"Saberspear","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":450,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Saberspear.webp","effects":[]}
{"_id":"kAnqkLwQT2s8V3tC","name":"Light Slugpistol","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 8
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":40,"long":160,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Light%20Slugpistol.webp","effects":[]}
{"_id":"kXba6Y0xOqeuT59S","name":"Shatter rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 120/480), Silent, Reload 2, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":120,"long":480,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6575001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Shatter%20Rifle.webp","effects":[]}
{"_id":"kiF9Ubs4yyOlqG7d","name":"Dual-phase saber","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Dexterity 11, Keen 1, Luminous, Versatile (1d10)
","chat":"","unidentified":""},"source":"","quantity":1,"weight":4,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":false,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":true,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7500001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Dual-Phase%20Saber.webp","effects":[]}
-{"_id":"lCCxKJ6dgNcKerQP","name":"Nervebaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Neuralizing 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"wis","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6998439,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"mVeMPN2VDbmxW2LH","name":"Echostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Double (1d6 kinetic), Finesse, Sonorous 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6775001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"lCCxKJ6dgNcKerQP","name":"Nervebaton","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Neuralizing 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"wis","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6998439,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Nervebaton.webp","effects":[]}
+{"_id":"mVeMPN2VDbmxW2LH","name":"Echostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Double (1d6 kinetic), Finesse, Sonorous 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":1200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6775001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Echostaff.webp","effects":[]}
{"_id":"mn4HW9ZpkObfamy4","name":"Lightclub","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":150,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3300001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Lightclub.webp","effects":[]}
{"_id":"nbBcy38sK2sejDo2","name":"Grenade launcher","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Rather than traditional power cells, the grenade launcher fires grenades. When firing a grenade at long range, or if you don’t meet the grenade launcher’s strength requirement, creatures within the radius of the grenade’s explosion have advantage on the saving throw. If you lack proficiency in the grenade launcher, you must roll the damage dice twice and take the lesser total.
\nAmmunition (range 80/320), Reload 1, Strength 11, Special, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":10,"price":800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"space"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6896876,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Grenade%20Launcher.webp","effects":[]}
{"_id":"nkHuIJ1RC1eLHPQ5","name":"Martial Lightsaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":"1d10 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":3800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Martial%20Lightsaber.webp","effects":[]}
{"_id":"ojGildpRHvmzXCXV","name":"Lightbow","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 60/240), Mighty, Piercing 1, Reload 4, Strength 11, Two-handed
","chat":"","unidentified":""},"source":"","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":60},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":true,"pic":true,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6996876,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Lightbow.webp","effects":[]}
{"_id":"ol1wR7Y9gTL4vWLN","name":"Bo-rifle (Staff)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"\n
The bo-rifle is a lasat weapon most commonly carried by the Honor Guard of Lasan, which has the unique property of functioning as both a rifle and a staff. On your turn, you can use your object interaction to switch between modes, detailed below.
\n
Rifle. While in this mode, the weapon uses traditional power cells.
\n
Staff. While in this mode, the weapon gains the shocking 13 property.
\n
\nRifle: 1d8 Energy, Ammunition (range 100/400), reload 6
\nStaff: 1d8 Kinetic, Double (1d8 kinetic), shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":2300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6975001,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bo%20Rifle.webp","effects":[]}
-{"_id":"pjMVd0d3xi1E2oe9","name":"Switch rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 60/240), Reload 8, Switch (1d6 lightning), Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":5500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"1d6 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799953,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"pjMVd0d3xi1E2oe9","name":"Switch Rifle","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 60/240), Reload 8, Switch (1d6 lightning), Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":5500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"1d6 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799953,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Rifle.webp","effects":[]}
{"_id":"qGACGd1CGJjFmyvC","name":"Vibrostaff","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":4,"price":100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":"2d4 + @mod"},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":true,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibrostaff.webp","effects":[]}
{"_id":"qVeMdnOgsk1EkUGN","name":"Rotary cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Rather than traditional power cells, the rotary cannon uses specialized power generator that allow it to fire continuously for 10 minutes. Replacing a power generator takes an action.
\nThe rotary cannon requires the use of a tripod unless you meet its strength requirement, which is included in the price. Over the course of 1 minute, you can deploy or collapse the rotary cannon on the tripod. While deployed, your speed is reduced to 0.
\n
\nAmmunition (range 100/400), Auto, Burst, Rapid, Special, Strength 19, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":76,"price":9800,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":10,"width":null,"units":"ft","type":"cube"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"save","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":true,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6799806,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Rotary%20Cannon.webp","effects":[]}
{"_id":"qcfcwHseK75zUy9U","name":"Assault Cannon","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Reload 8, Strength 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":24,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":200001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Assault%20Cannon.webp","effects":[]}
-{"_id":"rh4a2Bq5FJJ0c6bk","name":"Electrovoulge","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Reach, Shocking 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6787501,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"rh4a2Bq5FJJ0c6bk","name":"Electrovoulge","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Reach, Shocking 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":15,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6787501,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrovoulge.webp","effects":[]}
{"_id":"s02ZWo9cCBqTOx2A","name":"Vibroclaw","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Finesse, Fixed, Light
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":2,"price":600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":true,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6899977,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibroclaw.webp","effects":[]}
{"_id":"sGXUQb1kU45kKt1b","name":"Sniper Rifle","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 2, Strength 13
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":750,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":150,"long":600,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5000001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Sniper%20Rifle.webp","effects":[]}
{"_id":"sZo7FkpCKkRv6Dnw","name":"Incinerator pistol","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 60/240), Disintegrate 13, Reload 12, Strength 11
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":5,"price":2500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","fire"]],"versatile":""},"formula":"","save":{"ability":"con","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":true,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6675001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Incinerator%20Pistol.webp","effects":[]}
{"_id":"t8KzQluS5V5RSnI4","name":"Bo-rifle (Rifle)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"\n
The bo-rifle is a lasat weapon most commonly carried by the Honor Guard of Lasan, which has the unique property of functioning as both a rifle and a staff. On your turn, you can use your object interaction to switch between modes, detailed below.
\n
Rifle. While in this mode, the weapon uses traditional power cells.
\n
Staff. While in this mode, the weapon gains the shocking 13 property.
\n
\nRifle: 1d8 Energy, Ammunition (range 100/400), reload 6
\nStaff: 1d8 Kinetic, Double (1d8 kinetic), shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":7,"price":2300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":100,"long":400,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":40},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6975001,"flags":{},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bo%20Rifle.webp","effects":[]}
-{"_id":"tLHpQLnx7iYcLPtw","name":"War hat","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Disguised, Returning, Thrown (range 30/90)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7075001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"tLHpQLnx7iYcLPtw","name":"War hat","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Defensive 1, Disguised, Returning, Thrown (range 30/90)
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":3,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":30,"long":90,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":true,"dex":false,"dir":false,"drm":false,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":true,"shk":false,"sil":false,"spc":false,"str":false,"thr":true,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7075001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/War%20Hat.webp","effects":[]}
{"_id":"tXO7WB3V2wjjaBfE","name":"Shotgun","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 2, Reload 4, Strength 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":350,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Shotgun.webp","effects":[]}
-{"_id":"tlcCSePqUtbhMadM","name":"Chained lightdagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disarming, Finesse, Luminous, Reach, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7050001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"tlcCSePqUtbhMadM","name":"Chained Lightdagger","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disarming, Finesse, Luminous, Reach, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1700,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":10,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":true,"mig":false,"pic":false,"rap":false,"rch":true,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7050001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Lightweapons/Chained%20Lightdagger.webp","effects":[]}
{"_id":"u0mVXUeoHsFZ1pcG","name":"Blaster Cannon","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 2, Reload 4, Strength 15
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":36,"price":1600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":100,"long":400,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":120},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d12 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":true,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Blaster%20Cannon.webp","effects":[]}
-{"_id":"uGe5wQQJjZrQzltP","name":"Electrostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Double (1d6 kinetic), Finesse, Shocking 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6993751,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"uGe5wQQJjZrQzltP","name":"Electrostaff","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Double (1d6 kinetic), Finesse, Shocking 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":1100,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":true,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6993751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrostaff.webp","effects":[]}
{"_id":"uQ2AXesizBRcTjRl","name":"Ion Carbine","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 16
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":8,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d3 + @mod","ion"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Ion%20Carbine.webp","effects":[]}
{"_id":"v55dQl0raOAucwgP","name":"Vibromace","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":80,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibromace.webp","effects":[]}
{"_id":"w62Yd7ahdYyTH61q","name":"Shatter cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 80/320), Burst 4, Reload 8, Silent, Strength 15, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":24,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Shatter%20Cannon.webp","effects":[]}
-{"_id":"woDLArHK5OZHsTeU","name":"Disguised Blade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7150001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"woDLArHK5OZHsTeU","name":"Disguised Blade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7150001,"flags":{},"img":"systems/sw5e/packs/Icons/Disguised%20Blade,"effects":[]}
{"_id":"xfIWfVXfe5ZfD8S2","name":"IWS (Blaster)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"\n
The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.
\n
Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.
\n
Blaster. While in this mode, the weapon uses traditional power cells.
\n
Sniper. While in this mode, the weapon uses traditional power cells.
\n
\nAntiarmor: Special, Ammunition (range 60/240), reload 1, special
\nBlaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\nSniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
\nSpecial, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]}
{"_id":"y6faozksI3Bhwnpq","name":"Bowcaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Burst 4, Reload 4, Strength 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":50,"long":200,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bowcaster.webp","effects":[]}
-{"_id":"yVxRMON2OWIGeU4n","name":"Disruptorshiv","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Finesse, Shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6750001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
-{"_id":"yyBBJgGqeZ3Qx0OP","name":"Electrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Heavy, Shocking 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":18,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":4950001,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"yVxRMON2OWIGeU4n","name":"Disruptorshiv","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Disruptive, Finesse, Shocking 13
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6750001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Disruptorshiv.webp","effects":[]}
+{"_id":"yyBBJgGqeZ3Qx0OP","name":"Electrohammer","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Heavy, Shocking 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":18,"price":1400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["2d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":4950001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Electrohammer.webp","effects":[]}
{"_id":"z5Ms1OrmxDwLqGw8","name":"Shotosaber","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":500,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d6 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleLW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":true,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":4700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Lightweapons/Shotosaber.webp","effects":[]}
-{"_id":"z70LYISIcmv1gpJi","name":"Switch cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 80/320), Burst 4, Reload 8, Strength 11, Switch (2d4 acid/cold/fire/lightning), Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":26,"price":9600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"2d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999806,"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
+{"_id":"z70LYISIcmv1gpJi","name":"Switch Cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"Ammunition (range 80/320), Burst 4, Reload 8, Strength 11, Switch (2d4 acid/cold/fire/lightning), Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":26,"price":9600,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"2d4 + @mod","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999806,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Switch%20Cannon.webp","effects":[]}
{"_id":"z7LJqf8bjWnzqEw8","name":"Flechette cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"The flechette cannon does not make attack rolls. Rather than traditional power cells, the flechette cannon uses specialized cannon tanks, which, when fired, spray an area with the contents of the tank. Projector tanks require your target to make a saving throw to resist the tank’s effects. It can have different ammunition types loaded simultaneously, and you can choose which ammunition you’re using as you fire it (no action required). If you don’t meet the flechette cannon’s strength requirement, creatures have advantage on their saving throws. If you lack proficiency in the flechette cannon, you must roll the damage dice twice and take the lesser total.
\nAmmunition (range special), Reload 5, Special, Strength 11, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":16,"price":1800,"attunement":0,"equipped":false,"rarity":"","identified":false,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":true,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6793751,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Flechette%20Cannon.webp","effects":[]}
{"_id":"zArvVI9Nz8jA7vYF","name":"Vibroaxe","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Dexterity 11
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":11,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":true,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":5700001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Vibroaxe.webp","effects":[]}
{"_id":"zGU5Id8EUqoIzAmc","name":"Hold-Out","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"Reload 6
","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":250,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":30,"long":120,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":true,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Holdout%20Blaster.webp","effects":[]}
diff --git a/sw5e.js b/sw5e.js
index 5bef2a6f..3a1f6439 100644
--- a/sw5e.js
+++ b/sw5e.js
@@ -8,17 +8,17 @@
*/
// Import Modules
-import { SW5E } from "./module/config.js";
-import { registerSystemSettings } from "./module/settings.js";
-import { preloadHandlebarsTemplates } from "./module/templates.js";
-import { _getInitiativeFormula } from "./module/combat.js";
-import { measureDistances } from "./module/canvas.js";
+import {SW5E} from "./module/config.js";
+import {registerSystemSettings} from "./module/settings.js";
+import {preloadHandlebarsTemplates} from "./module/templates.js";
+import {_getInitiativeFormula} from "./module/combat.js";
+import {measureDistances} from "./module/canvas.js";
// Import Documents
import Actor5e from "./module/actor/entity.js";
import Item5e from "./module/item/entity.js";
import CharacterImporter from "./module/characterImporter.js";
-import { TokenDocument5e, Token5e } from "./module/token.js"
+import {TokenDocument5e, Token5e} from "./module/token.js";
// Import Applications
import AbilityTemplate from "./module/pixi/ability-template.js";
@@ -46,122 +46,137 @@ import * as migrations from "./module/migration.js";
/* Foundry VTT Initialization */
/* -------------------------------------------- */
-// Keep on while migrating to Foundry version 0.8
-CONFIG.debug.hooks = true;
+Hooks.once("init", function () {
+ console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
-Hooks.once("init", function() {
- console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
+ // Create a SW5E namespace within the game global
+ game.sw5e = {
+ applications: {
+ AbilityUseDialog,
+ ActorSheetFlags,
+ ActorSheet5eCharacter,
+ ActorSheet5eCharacterNew,
+ ActorSheet5eNPC,
+ ActorSheet5eNPCNew,
+ ActorSheet5eVehicle,
+ ItemSheet5e,
+ ShortRestDialog,
+ TraitSelector,
+ ActorMovementConfig,
+ ActorSensesConfig
+ },
+ canvas: {
+ AbilityTemplate
+ },
+ config: SW5E,
+ dice: dice,
+ entities: {
+ Actor5e,
+ Item5e,
+ TokenDocument5e,
+ Token5e
+ },
+ macros: macros,
+ migrations: migrations,
+ rollItemMacro: macros.rollItemMacro
+ };
- // Create a SW5E namespace within the game global
- game.sw5e = {
- applications: {
- AbilityUseDialog,
- ActorSheetFlags,
- ActorSheet5eCharacter,
- ActorSheet5eCharacterNew,
- ActorSheet5eNPC,
- ActorSheet5eNPCNew,
- ActorSheet5eVehicle,
- ItemSheet5e,
- ShortRestDialog,
- TraitSelector,
- ActorMovementConfig,
- ActorSensesConfig
- },
- canvas: {
- AbilityTemplate
- },
- config: SW5E,
- dice: dice,
- entities: {
- Actor5e,
- Item5e,
- TokenDocument5e,
- Token5e,
- },
- macros: macros,
- migrations: migrations,
- rollItemMacro: macros.rollItemMacro
- };
+ // Record Configuration Values
+ CONFIG.SW5E = SW5E;
+ CONFIG.Actor.documentClass = Actor5e;
+ CONFIG.Item.documentClass = Item5e;
+ CONFIG.Token.documentClass = TokenDocument5e;
+ CONFIG.Token.objectClass = Token5e;
+ CONFIG.time.roundTime = 6;
+ CONFIG.fontFamilies = ["Engli-Besh", "Open Sans", "Russo One"];
- // Record Configuration Values
- CONFIG.SW5E = SW5E;
- CONFIG.Actor.documentClass = Actor5e;
- CONFIG.Item.documentClass = Item5e;
- CONFIG.Token.documentClass = TokenDocument5e;
- CONFIG.Token.objectClass = Token5e;
- CONFIG.time.roundTime = 6;
- CONFIG.fontFamilies = [
- "Engli-Besh",
- "Open Sans",
- "Russo One"
- ];
+ CONFIG.Dice.DamageRoll = dice.DamageRoll;
+ CONFIG.Dice.D20Roll = dice.D20Roll;
- CONFIG.Dice.DamageRoll = dice.DamageRoll;
- CONFIG.Dice.D20Roll = dice.D20Roll;
+ // 5e cone RAW should be 53.13 degrees
+ CONFIG.MeasuredTemplate.defaults.angle = 53.13;
- // 5e cone RAW should be 53.13 degrees
- CONFIG.MeasuredTemplate.defaults.angle = 53.13;
+ // Add DND5e namespace for module compatability
+ game.dnd5e = game.sw5e;
+ CONFIG.DND5E = CONFIG.SW5E;
- // Add DND5e namespace for module compatability
- game.dnd5e = game.sw5e;
- CONFIG.DND5E = CONFIG.SW5E;
+ // Register System Settings
+ registerSystemSettings();
- // Register System Settings
- registerSystemSettings();
+ // Patch Core Functions
+ CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
+ Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
- // Patch Core Functions
- CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
- Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
+ // Register Roll Extensions
+ CONFIG.Dice.rolls.push(dice.D20Roll);
+ CONFIG.Dice.rolls.push(dice.DamageRoll);
- // Register Roll Extensions
- CONFIG.Dice.rolls.push(dice.D20Roll);
- CONFIG.Dice.rolls.push(dice.DamageRoll);
+ // Register sheet application classes
+ Actors.unregisterSheet("core", ActorSheet);
+ Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, {
+ types: ["character"],
+ makeDefault: true,
+ label: "SW5E.SheetClassCharacter"
+ });
+ Actors.registerSheet("sw5e", ActorSheet5eCharacter, {
+ types: ["character"],
+ makeDefault: false,
+ label: "SW5E.SheetClassCharacterOld"
+ });
+ Actors.registerSheet("sw5e", ActorSheet5eNPCNew, {
+ types: ["npc"],
+ makeDefault: true,
+ label: "SW5E.SheetClassNPC"
+ });
+ Actors.registerSheet("sw5e", ActorSheet5eNPC, {
+ types: ["npc"],
+ makeDefault: false,
+ label: "SW5E.SheetClassNPCOld"
+ });
+ // Actors.registerSheet("sw5e", ActorSheet5eStarship, {
+ // types: ["starship"],
+ // makeDefault: true,
+ // label: "SW5E.SheetClassStarship"
+ // });
+ Actors.registerSheet("sw5e", ActorSheet5eVehicle, {
+ types: ["vehicle"],
+ makeDefault: true,
+ label: "SW5E.SheetClassVehicle"
+ });
+ Items.unregisterSheet("core", ItemSheet);
+ Items.registerSheet("sw5e", ItemSheet5e, {
+ types: [
+ "weapon",
+ "equipment",
+ "consumable",
+ "tool",
+ "loot",
+ "class",
+ "power",
+ "feat",
+ "species",
+ "backpack",
+ "archetype",
+ "classfeature",
+ "background",
+ "fightingmastery",
+ "fightingstyle",
+ "lightsaberform",
+ "deployment",
+ "deploymentfeature",
+ "starship",
+ "starshipfeature",
+ "starshipmod",
+ "venture"
+ ],
+ makeDefault: true,
+ label: "SW5E.SheetClassItem"
+ });
- // Register sheet application classes
- Actors.unregisterSheet("core", ActorSheet);
- Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, {
- types: ["character"],
- makeDefault: true,
- label: "SW5E.SheetClassCharacter"
- });
- Actors.registerSheet("sw5e", ActorSheet5eCharacter, {
- types: ["character"],
- makeDefault: false,
- label: "SW5E.SheetClassCharacterOld"
- });
- Actors.registerSheet("sw5e", ActorSheet5eNPCNew, {
- types: ["npc"],
- makeDefault: true,
- label: "SW5E.SheetClassNPC"
- });
- Actors.registerSheet("sw5e", ActorSheet5eNPC, {
- types: ["npc"],
- makeDefault: false,
- label: "SW5E.SheetClassNPCOld"
- });
- // Actors.registerSheet("sw5e", ActorSheet5eStarship, {
- // types: ["starship"],
- // makeDefault: true,
- // label: "SW5E.SheetClassStarship"
- // });
- Actors.registerSheet('sw5e', ActorSheet5eVehicle, {
- types: ['vehicle'],
- makeDefault: true,
- label: "SW5E.SheetClassVehicle"
- });
- Items.unregisterSheet("core", ItemSheet);
- Items.registerSheet("sw5e", ItemSheet5e, {
- types: ['weapon', 'equipment', 'consumable', 'tool', 'loot', 'class', 'power', 'feat', 'species', 'backpack', 'archetype', 'classfeature', 'background', 'fightingmastery', 'fightingstyle', 'lightsaberform', 'deployment', 'deploymentfeature', 'starship', 'starshipfeature', 'starshipmod', 'venture'],
- makeDefault: true,
- label: "SW5E.SheetClassItem"
- });
-
- // Preload Handlebars Templates
- return preloadHandlebarsTemplates();
+ // Preload Handlebars Templates
+ return preloadHandlebarsTemplates();
});
-
/* -------------------------------------------- */
/* Foundry VTT Setup */
/* -------------------------------------------- */
@@ -169,131 +184,175 @@ Hooks.once("init", function() {
/**
* This function runs after game data has been requested and loaded from the servers, so entities exist
*/
-Hooks.once("setup", function() {
+Hooks.once("setup", function () {
+ // Localize CONFIG objects once up-front
+ const toLocalize = [
+ "abilities",
+ "abilityAbbreviations",
+ "abilityActivationTypes",
+ "abilityConsumptionTypes",
+ "actorSizes",
+ "alignments",
+ "armorProficiencies",
+ "armorPropertiesTypes",
+ "conditionTypes",
+ "consumableTypes",
+ "cover",
+ "currencies",
+ "damageResistanceTypes",
+ "damageTypes",
+ "distanceUnits",
+ "equipmentTypes",
+ "healingTypes",
+ "itemActionTypes",
+ "languages",
+ "limitedUsePeriods",
+ "movementTypes",
+ "movementUnits",
+ "polymorphSettings",
+ "proficiencyLevels",
+ "senses",
+ "skills",
+ "starshipRolessm",
+ "starshipRolesmed",
+ "starshipRoleslg",
+ "starshipRoleshuge",
+ "starshipRolesgrg",
+ "starshipSkills",
+ "powerComponents",
+ "powerLevels",
+ "powerPreparationModes",
+ "powerScalingModes",
+ "powerSchools",
+ "targetTypes",
+ "timePeriods",
+ "toolProficiencies",
+ "weaponProficiencies",
+ "weaponProperties",
+ "weaponSizes",
+ "weaponTypes"
+ ];
- // Localize CONFIG objects once up-front
- const toLocalize = [
- "abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments",
- "armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes",
- "damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages",
- "limitedUsePeriods", "movementTypes", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills",
- "starshipRolessm", "starshipRolesmed", "starshipRoleslg", "starshipRoleshuge", "starshipRolesgrg", "starshipSkills",
- "powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes",
- "timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponSizes", "weaponTypes"
- ];
+ // Exclude some from sorting where the default order matters
+ const noSort = [
+ "abilities",
+ "alignments",
+ "currencies",
+ "distanceUnits",
+ "movementUnits",
+ "itemActionTypes",
+ "proficiencyLevels",
+ "limitedUsePeriods",
+ "powerComponents",
+ "powerLevels",
+ "powerPreparationModes",
+ "weaponTypes"
+ ];
- // Exclude some from sorting where the default order matters
- const noSort = [
- "abilities", "alignments", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels",
- "limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes"
- ];
-
- // Localize and sort CONFIG objects
- for ( let o of toLocalize ) {
- const localized = Object.entries(CONFIG.SW5E[o]).map(e => {
- return [e[0], game.i18n.localize(e[1])];
- });
- if ( !noSort.includes(o) ) localized.sort((a, b) => a[1].localeCompare(b[1]));
- CONFIG.SW5E[o] = localized.reduce((obj, e) => {
- obj[e[0]] = e[1];
- return obj;
- }, {});
- }
- // add DND5E translation for module compatability
- game.i18n.translations.DND5E = game.i18n.translations.SW5E;
- // console.log(game.settings.get("sw5e", "colorTheme"));
- let theme = game.settings.get("sw5e", "colorTheme") + '-theme';
- document.body.classList.add(theme);
+ // Localize and sort CONFIG objects
+ for (let o of toLocalize) {
+ const localized = Object.entries(CONFIG.SW5E[o]).map((e) => {
+ return [e[0], game.i18n.localize(e[1])];
+ });
+ if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1]));
+ CONFIG.SW5E[o] = localized.reduce((obj, e) => {
+ obj[e[0]] = e[1];
+ return obj;
+ }, {});
+ }
+ // add DND5E translation for module compatability
+ game.i18n.translations.DND5E = game.i18n.translations.SW5E;
+ // console.log(game.settings.get("sw5e", "colorTheme"));
+ let theme = game.settings.get("sw5e", "colorTheme") + "-theme";
+ document.body.classList.add(theme);
});
/* -------------------------------------------- */
/**
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
*/
-Hooks.once("ready", function() {
+Hooks.once("ready", function () {
+ // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
+ Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
- // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
- Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
+ // Determine whether a system migration is required and feasible
+ if (!game.user.isGM) return;
+ const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
+ const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6";
+ // Check for R1 SW5E versions
+ const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6";
+ const COMPATIBLE_MIGRATION_VERSION = 0.8;
+ const needsMigration =
+ currentVersion &&
+ (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) ||
+ isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
+ if (!needsMigration && needsMigration !== "") return;
- // Determine whether a system migration is required and feasible
- if ( !game.user.isGM ) return;
- const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
- const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6";
- // Check for R1 SW5E versions
- const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6";
- const COMPATIBLE_MIGRATION_VERSION = 0.80;
- const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
- if (!needsMigration && needsMigration !== "") return;
-
- // Perform the migration
- if ( currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion) ) {
- const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`;
- ui.notifications.error(warning, {permanent: true});
- }
- migrations.migrateWorld();
+ // Perform the migration
+ if (currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion)) {
+ const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`;
+ ui.notifications.error(warning, {permanent: true});
+ }
+ migrations.migrateWorld();
});
/* -------------------------------------------- */
/* Canvas Initialization */
/* -------------------------------------------- */
-Hooks.on("canvasInit", function() {
- // Extend Diagonal Measurement
- canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
- SquareGrid.prototype.measureDistances = measureDistances;
+Hooks.on("canvasInit", function () {
+ // Extend Diagonal Measurement
+ canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
+ SquareGrid.prototype.measureDistances = measureDistances;
});
-
/* -------------------------------------------- */
/* Other Hooks */
/* -------------------------------------------- */
Hooks.on("renderChatMessage", (app, html, data) => {
+ // Display action buttons
+ chat.displayChatActionButtons(app, html, data);
- // Display action buttons
- chat.displayChatActionButtons(app, html, data);
+ // Highlight critical success or failure die
+ chat.highlightCriticalSuccessFailure(app, html, data);
- // Highlight critical success or failure die
- chat.highlightCriticalSuccessFailure(app, html, data);
-
- // Optionally collapse the content
- if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide();
+ // Optionally collapse the content
+ if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide();
});
Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
-Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions);
-Hooks.on("renderSceneDirectory", (app, html, data)=> {
- //console.log(html.find("header.folder-header"));
- setFolderBackground(html);
+Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions);
+Hooks.on("renderSceneDirectory", (app, html, data) => {
+ //console.log(html.find("header.folder-header"));
+ setFolderBackground(html);
});
-Hooks.on("renderActorDirectory", (app, html, data)=> {
- setFolderBackground(html);
- CharacterImporter.addImportButton(html);
+Hooks.on("renderActorDirectory", (app, html, data) => {
+ setFolderBackground(html);
+ CharacterImporter.addImportButton(html);
});
-Hooks.on("renderItemDirectory", (app, html, data)=> {
- setFolderBackground(html);
+Hooks.on("renderItemDirectory", (app, html, data) => {
+ setFolderBackground(html);
});
-Hooks.on("renderJournalDirectory", (app, html, data)=> {
- setFolderBackground(html);
+Hooks.on("renderJournalDirectory", (app, html, data) => {
+ setFolderBackground(html);
});
-Hooks.on("renderRollTableDirectory", (app, html, data)=> {
- setFolderBackground(html);
+Hooks.on("renderRollTableDirectory", (app, html, data) => {
+ setFolderBackground(html);
});
Hooks.on("ActorSheet5eCharacterNew", (app, html, data) => {
- console.log("renderSwaltSheet");
+ console.log("renderSwaltSheet");
});
// FIXME: This helper is needed for the vehicle sheet. It should probably be refactored.
-Handlebars.registerHelper('getProperty', function (data, property) {
- return getProperty(data, property);
+Handlebars.registerHelper("getProperty", function (data, property) {
+ return getProperty(data, property);
});
-
function setFolderBackground(html) {
- html.find("header.folder-header").each(function() {
- let bgColor = $(this).css("background-color");
- if(bgColor == undefined)
- bgColor = "rgb(255,255,255)";
- $(this).closest('li').css("background-color", bgColor);
- })
-}
\ No newline at end of file
+ html.find("header.folder-header").each(function () {
+ let bgColor = $(this).css("background-color");
+ if (bgColor == undefined) bgColor = "rgb(255,255,255)";
+ $(this).closest("li").css("background-color", bgColor);
+ });
+}
diff --git a/system.json b/system.json
index ac8b4dc2..3a0a82ca 100644
--- a/system.json
+++ b/system.json
@@ -2,7 +2,7 @@
"name": "sw5e",
"title": "SW 5th Edition",
"description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.",
- "version": "1.3.5.R1-A6",
+ "version": "1.3.5.R1-A7",
"author": "Dev Team",
"scripts": [],
"esmodules": ["sw5e.js"],
@@ -152,7 +152,7 @@
"primaryTokenAttribute": "attributes.hp",
"secondaryTokenAttribute": null,
"minimumCoreVersion": "0.8.2",
- "compatibleCoreVersion": "0.8.7",
+ "compatibleCoreVersion": "0.8.8",
"url": "https://github.com/unrealkakeman89/sw5e",
"manifest": "https://raw.githubusercontent.com/unrealkakeman89/sw5e/master/system.json",
"download": "https://github.com/unrealkakeman89/sw5e/archive/master.zip"