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 ad37ab56..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,1806 +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';
-
- // 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;
- }
+ // 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 === 'character' || actorData.type === 'npc')) return;
+ // Reset class store to ensure it is updated with any changes
+ this._classes = undefined;
- const ad = actorData.data;
+ // 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;
- // 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 }
- }
- });
- 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.
';
- 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/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);
+ });
+}