diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 4ca0a625..00000000
--- a/.prettierrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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 cac0e10e..7a73a41b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,2 @@
{
- "editor.formatOnSave": true
}
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
index d18fc7d3..dabc0757 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 0fe01203..ad37ab56 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,1875 +10,1806 @@ 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;
- /* -------------------------------------------- */
- /* Properties */
- /* -------------------------------------------- */
+ /**
+ * The data source for Actor5e.classes allowing it to be lazily computed.
+ * @type {Object}
+ * @private
+ */
+ _classes = undefined;
- /**
- * 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;
- }, {}));
+ /* -------------------------------------------- */
+ /* 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);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /** @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;
- /**
- * Is this Actor currently polymorphed into some other creature?
- * @type {boolean}
- */
- get isPolymorphed() {
- return this.getFlag("sw5e", "isPolymorphed") || false;
+ // If we merged saves when transforming, take the highest bonus here.
+ if (originalSaves && abl.proficient) {
+ abl.save = Math.max(abl.save, originalSaves[id].save);
+ }
}
- /* -------------------------------------------- */
- /* Methods */
- /* -------------------------------------------- */
+ // Inventory encumbrance
+ data.attributes.encumbrance = this._computeEncumbrance(actorData);
- /** @override */
- prepareData() {
- super.prepareData();
+ // Prepare skills
+ this._prepareSkills(actorData, bonuses, checkBonus, originalSkills);
- // iterate over owned items and recompute attributes that depend on prepared actor data
- this.items.forEach((item) => item.prepareFinalAttributes());
+ // 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);
+ }
- /** @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);
- }
+ /* -------------------------------------------- */
+
+ /**
+ * 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});
+ }
- /** @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;
- }
- }
- }
+ /**
+ * 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)) || [];
+ }
- // 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);
- }
- }
+ /**
+ * 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();
- // Inventory encumbrance
- data.attributes.encumbrance = this._computeEncumbrance(actorData);
+ // Get the configuration of features which may be added
+ const clsConfig = CONFIG.SW5E.classFeatures[className];
+ if (!clsConfig) return [];
- // 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);
+ // 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);
}
- /* -------------------------------------------- */
-
- /**
- * 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)];
+ // 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);
}
- /* -------------------------------------------- */
-
- /**
- * 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];
+ // 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;
+ }
- /** @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;
+ /* -------------------------------------------- */
+ /* 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; }
}
- /* -------------------------------------------- */
-
- /**
- * 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});
+ 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];
+ }
}
- /* -------------------------------------------- */
- /**
- * 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)) || [];
+ // 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];
}
- /* -------------------------------------------- */
-
- /**
- * 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;
+ 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];
}
- /* -------------------------------------------- */
- /* 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);
+ 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);
+ }
}
- /* -------------------------------------------- */
-
- /**
- * 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);
- }
+ // 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;
+ }
}
- /* -------------------------------------------- */
- /**
- * Prepare vehicle type-specific data
- * @param actorData
- * @private
- */
- _prepareVehicleData(actorData) {}
+ // 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 starship type-specific data
- * @param actorData
- * @private
- */
- _prepareStarshipData(actorData) {
- const data = actorData.data;
+ /**
+ * Prepare data related to the power-casting capabilities of the Actor
+ * @private
+ */
+ _computeDerivedPowercasting (actorData) {
- // Proficiency
- data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4);
+ if (!(actorData.type === 'character' || actorData.type === 'npc')) return;
- // 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;
+ 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;
}
- /* -------------------------------------------- */
+ }
- /**
- * 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 || {};
+ /**
+ * 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);
- // 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;
- }
+ // [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);
- /**
- * 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";
+ // 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) };
+ }
- // 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
- };
+ /* -------------------------------------------- */
+ /* Event Handlers */
+ /* -------------------------------------------- */
- // 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;
- }
- }
+ /** @inheritdoc */
+ async _preCreate(data, options, user) {
+ await super._preCreate(data, options, user);
- 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];
- }
- }
+ // Token size category
+ const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"];
+ this.data.token.update({width: s, height: s});
- // 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];
- }
+ // Player character configuration
+ if ( this.type === "character" ) {
+ this.data.token.update({vision: true, actorLink: true, disposition: 1});
+ }
+ }
- 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];
- }
+ /** @inheritdoc */
+ async _preUpdate(changed, options, user) {
+ await super._preUpdate(changed, options, user);
- 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;
- }
+ // 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);
+ }
+ }
- /**
- * 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;
+ /**
+ * 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});
+ }
- // 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;
+ /* -------------------------------------------- */
+ /* Gameplay Mechanics */
+ /* -------------------------------------------- */
- if (actorData.type !== "character") return;
+ /** @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);
+ }
- // 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;
- }
+ /* -------------------------------------------- */
+
+ /**
+ * 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");
}
- /* -------------------------------------------- */
-
- /**
- * 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};
+ // Skill check bonus
+ if ( bonuses.skill ) {
+ data["skillBonus"] = bonuses.skill;
+ parts.push("@skillBonus");
}
- /* -------------------------------------------- */
- /* 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});
- }
+ // 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"));
- /** @inheritdoc */
- async _preUpdate(changed, options, user) {
- await super._preUpdate(changed, options, user);
+ // 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);
+ }
- // 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;
- }
+ /* -------------------------------------------- */
+
+ /**
+ * 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);
+ }
- // 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);
- }
+ /* -------------------------------------------- */
+
+ /**
+ * 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);
}
- /* -------------------------------------------- */
-
- /**
- * 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});
+ // Add global actor bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if ( bonuses.check ) {
+ parts.push("@checkBonus");
+ data.checkBonus = bonuses.check;
}
- /* -------------------------------------------- */
- /* 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);
+ // 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 }
+ }
+ });
+ return d20Roll(rollData);
+ }
- /**
- * 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;
+ /**
+ * 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];
- // Remaining goes to health
- const tmpMax = parseInt(hp.tempmax) || 0;
- const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax);
+ // Construct parts
+ const parts = ["@mod"];
+ const data = {mod: abl.mod};
- // 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;
+ // Include proficiency bonus
+ if ( abl.prof > 0 ) {
+ parts.push("@prof");
+ data.prof = abl.prof;
}
- /* -------------------------------------------- */
+ // Include a global actor ability save bonus
+ const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
+ if ( bonuses.save ) {
+ parts.push("@saveBonus");
+ data.saveBonus = bonuses.save;
+ }
- /**
- * 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") || {};
+ // Add provided extra roll parts now because they will get clobbered by mergeObject below
+ if (options.parts?.length > 0) {
+ parts.push(...options.parts);
+ }
- // Compose roll parts and data
- const parts = ["@mod"];
- const data = {mod: skl.mod + skl.prof};
+ // 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);
+ }
- // 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");
- }
+ /**
+ * 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={}) {
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
- }
+ // 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;
+ }
- // Reliable Talent applies to any skill check we have full or better proficiency in
- const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent");
+ // Evaluate a global saving throw bonus
+ const parts = [];
+ const data = {};
- // 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}
- }
+ // Include a global actor ability save bonus
+ const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {};
+ if ( bonuses.save ) {
+ parts.push("@saveBonus");
+ data.saveBonus = bonuses.save;
+ }
+
+ // Evaluate the roll
+ const rollData = foundry.utils.mergeObject(options, {
+ parts: parts,
+ data: data,
+ title: game.i18n.localize("SW5E.DeathSavingThrow"),
+ halflingLucky: this.getFlag("sw5e", "halflingLucky"),
+ targetValue: 10,
+ messageData: {
+ speaker: options.speaker || ChatMessage.getSpeaker({actor: this}),
+ "flags.sw5e.roll": {type: "death"}
+ }
+ });
+ const roll = await d20Roll(rollData);
+ if ( !roll ) return null;
+
+ // Take action depending on the result
+ const success = roll.total >= 10;
+ const d20 = roll.dice[0].total;
+
+ let chatString;
+
+ // Save success
+ if ( success ) {
+ let successes = (death.success || 0) + 1;
+
+ // Critical Success = revive with 1hp
+ if ( d20 === 20 ) {
+ await this.update({
+ "data.attributes.death.success": 0,
+ "data.attributes.death.failure": 0,
+ "data.attributes.hp.value": 1
});
- return d20Roll(rollData);
- }
+ chatString = "SW5E.DeathSaveCriticalSuccess";
+ }
- /* -------------------------------------------- */
-
- /**
- * 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}
- }
+ // 3 Successes = survive and reset checks
+ else if ( successes === 3 ) {
+ await this.update({
+ "data.attributes.death.success": 0,
+ "data.attributes.death.failure": 0
});
- return d20Roll(rollData);
+ chatString = "SW5E.DeathSaveSuccess";
+ }
+
+ // Increment successes
+ else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)});
}
- /* -------------------------------------------- */
-
- /**
- * Roll an Ability Saving Throw
- * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus
- * @param {String} abilityId The ability ID (e.g. "str")
- * @param {Object} options Options which configure how ability tests are rolled
- * @return {Promise} A Promise which resolves to the created Roll instance
- */
- rollAbilitySave(abilityId, options = {}) {
- const label = CONFIG.SW5E.abilities[abilityId];
- const abl = this.data.data.abilities[abilityId];
-
- // Construct parts
- const parts = ["@mod"];
- const data = {mod: abl.mod};
-
- // Include proficiency bonus
- if (abl.prof > 0) {
- parts.push("@prof");
- data.prof = abl.prof;
- }
-
- // Include a global actor ability save bonus
- const bonuses = getProperty(this.data.data, "bonuses.abilities") || {};
- if (bonuses.save) {
- parts.push("@saveBonus");
- data.saveBonus = bonuses.save;
- }
-
- // Add provided extra roll parts now because they will get clobbered by mergeObject below
- if (options.parts?.length > 0) {
- parts.push(...options.parts);
- }
-
- // Roll and return
- const rollData = 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);
+ // 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";
+ }
}
- /* -------------------------------------------- */
-
- /**
- * Perform a death saving throw, rolling a d20 plus any global save bonuses
- * @param {Object} options Additional options which modify the roll
- * @return {Promise} A Promise which resolves to the Roll instance
- */
- async rollDeathSave(options = {}) {
- // Display a warning if we are not at zero HP or if we already have reached 3
- const death = this.data.data.attributes.death;
- if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) {
- ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary"));
- return null;
- }
-
- // Evaluate a global saving throw bonus
- const parts = [];
- const data = {};
-
- // Include a global actor ability save bonus
- const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {};
- if (bonuses.save) {
- parts.push("@saveBonus");
- data.saveBonus = bonuses.save;
- }
-
- // Evaluate the roll
- const rollData = foundry.utils.mergeObject(options, {
- parts: parts,
- data: data,
- title: game.i18n.localize("SW5E.DeathSavingThrow"),
- halflingLucky: this.getFlag("sw5e", "halflingLucky"),
- targetValue: 10,
- messageData: {
- "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
- "flags.sw5e.roll": {type: "death"}
- }
- });
- const roll = await d20Roll(rollData);
- if (!roll) return null;
-
- // Take action depending on the result
- const success = roll.total >= 10;
- const d20 = roll.dice[0].total;
-
- let chatString;
-
- // Save success
- if (success) {
- let successes = (death.success || 0) + 1;
-
- // Critical Success = revive with 1hp
- if (d20 === 20) {
- await this.update({
- "data.attributes.death.success": 0,
- "data.attributes.death.failure": 0,
- "data.attributes.hp.value": 1
- });
- chatString = "SW5E.DeathSaveCriticalSuccess";
- }
-
- // 3 Successes = survive and reset checks
- else if (successes === 3) {
- await this.update({
- "data.attributes.death.success": 0,
- "data.attributes.death.failure": 0
- });
- chatString = "SW5E.DeathSaveSuccess";
- }
-
- // Increment successes
- else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)});
- }
-
- // Save failure
- else {
- let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1);
- await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)});
- if (failures >= 3) {
- // 3 Failures = death
- chatString = "SW5E.DeathSaveFailure";
- }
- }
-
- // Display success/failure chat message
- if (chatString) {
- let chatData = {content: game.i18n.format(chatString, {name: this.name}), speaker};
- ChatMessage.applyRollMode(chatData, roll.options.rollMode);
- await ChatMessage.create(chatData);
- }
-
- // Return the rolled result
- return roll;
+ // Display success/failure chat message
+ if ( chatString ) {
+ let chatData = { content: game.i18n.format(chatString, {name: this.name}), speaker };
+ ChatMessage.applyRollMode(chatData, roll.options.rollMode);
+ await ChatMessage.create(chatData);
}
- /* -------------------------------------------- */
+ // Return the rolled result
+ return roll;
+ }
- /**
- * 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);
- });
- }
+ /**
+ * 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 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;
+ // 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;
}
- /* -------------------------------------------- */
-
- /**
- * 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);
-
- // 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);
+ 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 0bb7f64b..ba3cfe06 100644
--- a/module/item/sheet.js
+++ b/module/item/sheet.js
@@ -1,370 +1,361 @@
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;
+ // 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})`;
}
+ return obj;
+ }, {});
}
- /* -------------------------------------------- */
-
- /** @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})`}
- );
+ // 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;
}
- // 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;
- }, {});
- }
+ // 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 {};
+ }
- // 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;
- }, {});
- }
+ /* -------------------------------------------- */
- // 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 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");
+ }
+ }
- // 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 {};
+ /* -------------------------------------------- */
+
+ /**
+ * 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);
}
- /* -------------------------------------------- */
-
- /**
- * 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 type
+ if (item.data.actionType) {
+ props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
}
- /* -------------------------------------------- */
+ // 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;
+ /* -------------------------------------------- */
- 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);
- }
+ /**
+ * 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")
+ );
+ }
- // Action type
- if (item.data.actionType) {
- props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
- }
+ /* -------------------------------------------- */
- // 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);
+ /** @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([["", ""]]) });
}
- /* -------------------------------------------- */
-
- /**
- * 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")
- );
+ // 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 });
}
+ }
- /* -------------------------------------------- */
+ /* -------------------------------------------- */
- /** @inheritdoc */
- setPosition(position = {}) {
- if (!(this._minimized || position.height)) {
- position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
- }
- return super.setPosition(position);
+ /**
+ * 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);
+ }
- /* -------------------------------------------- */
- /* 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([["", ""]])});
- }
-
- // 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);
- }
+ /** @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 e8919864..96bb3419 100644
--- a/module/macros.js
+++ b/module/macros.js
@@ -1,3 +1,4 @@
+
/* -------------------------------------------- */
/* Hotbar Macros */
/* -------------------------------------------- */
@@ -10,24 +11,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;
}
/* -------------------------------------------- */
@@ -39,22 +40,20 @@ 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 ab6eb420..e549e934 100644
--- a/module/migration.js
+++ b/module/migration.js
@@ -2,68 +2,65 @@
* 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});
};
/* -------------------------------------------- */
@@ -73,48 +70,50 @@ 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}`);
- } catch (err) {
- // Handle migration failures
- err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
- console.error(err);
- }
+ // Save the entry, if data was changed
+ await doc.update(updateData);
+ console.log(`Migrated ${entity} entity ${doc.name} in 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}`);
+ // 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}`);
};
/* -------------------------------------------- */
@@ -127,82 +126,84 @@ 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 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 data
+ const model = game.system.model.Actor[actorData.type];
+ actorData.data = filterObject(actorData.data, model);
- // Return the scrubbed data
- return actorData;
+ // 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;
}
+
/* -------------------------------------------- */
/**
@@ -211,11 +212,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;
};
/* -------------------------------------------- */
@@ -225,12 +226,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;
};
/* -------------------------------------------- */
@@ -241,34 +242,33 @@ 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,93 +284,87 @@ export const migrateSceneData = async function (scene) {
* @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));
- newPowers.push(newPower);
- }
- }
- }
+ 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));
- // get actor to create new powers
- const liveActor = game.actors.get(actor._id);
- // create the powers on the actor
- liveActor.createEmbeddedEntity("OwnedItem", newPowers);
+ newPowers.push(newPower);
+ }
+ }
+ }
- // 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");
- });
- }
+ // get actor to create new powers
+ const liveActor = game.actors.get(actor._id);
+ // create the powers on the actor
+ liveActor.createEmbeddedEntity("OwnedItem", newPowers);
- //merge object
- actorData = mergeObject(actorData, updateData);
- // Return the scrubbed data
- return actor;
+ // 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;
}
+
/**
* 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) {
- // 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;
- }
+ // 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 ) {
- // Remove the old attribute
- updateData["data.attributes.-=speed"] = 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;
}
- return updateData;
+
+ // Remove the old attribute
+ updateData["data.attributes.-=speed"] = null;
+ }
+ return updateData
}
/* -------------------------------------------- */
@@ -380,58 +374,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 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 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"] = "";
+ }
- // 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"] = "";
- }
+ // Remove the Power DC Bonus
+ updateData["data.bonuses.power.-=dc"] = null;
- // Remove the Power DC Bonus
- updateData["data.bonuses.power.-=dc"] = null;
-
- return updateData;
+ return updateData
}
/* -------------------------------------------- */
@@ -441,35 +435,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;
}
/* -------------------------------------------- */
@@ -479,77 +473,76 @@ 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) {
- // 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() || "";
+ // 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 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 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() || "";
- // No match found
- else {
- data.value = "custom";
- data.custom = original;
- }
+ // 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 = "";
}
- // Update the actor data
- updateData["data.details.type"] = data;
- return updateData;
+ // No match found
+ else {
+ data.value = "custom";
+ data.custom = original;
+ }
+ }
+
+ // Update the actor data
+ updateData["data.details.type"] = data;
+ return updateData;
}
/* -------------------------------------------- */
@@ -558,41 +551,42 @@ 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;
}
/* -------------------------------------------- */
@@ -604,45 +598,42 @@ 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 => {
-
- //})
+ //})
}
/* -------------------------------------------- */
@@ -656,10 +647,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;
}
/* -------------------------------------------- */
@@ -670,41 +661,43 @@ 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;
- });
- }
- await pack.updateEntity(update, {recursive: false});
- console.log(`Purged flags from ${entity.name}`);
+ 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.configure({locked: true});
+ await pack.updateEntity(update, {recursive: false});
+ console.log(`Purged flags from ${entity.name}`);
+ }
+ 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 6799eaec..3af99181 100644
--- a/module/pixi/ability-template.js
+++ b/module/pixi/ability-template.js
@@ -1,132 +1,133 @@
-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;
- // 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
- };
+ /**
+ * 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;
- // 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;
- }
+ // 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
+ };
- // 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;
+ // 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;
+ }
- /**
- * 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);
+ /**
+ * Creates a preview of the power template
+ */
+ drawPreview() {
+ const initialLayer = canvas.activeLayer;
- // Hide the sheet that originated the preview
- if (this.actorSheet) this.actorSheet.minimize();
+ // Draw the template and switch to the template layer
+ this.draw();
+ this.layer.activate();
+ this.layer.preview.addChild(this);
- // Activate interactivity
- this.activatePreviewListeners(initialLayer);
- }
+ // Hide the sheet that originated the preview
+ if ( this.actorSheet ) this.actorSheet.minimize();
- /* -------------------------------------------- */
+ // 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;
+ /* -------------------------------------------- */
- // 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;
- };
+ /**
+ * 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;
- // 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();
- };
+ // 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;
+ };
- // 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]);
- };
+ // 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();
+ };
- // 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();
- };
+ // 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]);
+ };
- // 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;
- }
+ // 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;
+ }
}
diff --git a/module/settings.js b/module/settings.js
index 3b765c69..2adde8d3 100644
--- a/module/settings.js
+++ b/module/settings.js
@@ -1,144 +1,145 @@
-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
- });
+export const registerSystemSettings = function() {
- /**
- * 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"
- }
- });
+ /**
+ * 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 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 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 Initiative formula setting
- */
- game.settings.register("sw5e", "initiativeDexTiebreaker", {
- name: "SETTINGS.5eInitTBN",
- hint: "SETTINGS.5eInitTBL",
- scope: "world",
- config: true,
- default: false,
- type: Boolean
- });
+ /**
+ * 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
+ });
- /**
- * Require Currency Carrying Weight
- */
- game.settings.register("sw5e", "currencyWeight", {
- name: "SETTINGS.5eCurWtN",
- hint: "SETTINGS.5eCurWtL",
- scope: "world",
- config: true,
- default: true,
- 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
+ });
- /**
- * 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
- });
+ /**
+ * 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 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 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 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 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();
+ }
+ });
- /**
- * 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"
- }
- });
+ /**
+ * 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"
+ }
+ });
};
diff --git a/module/templates.js b/module/templates.js
index e810b6da..a27bdf71 100644
--- a/module/templates.js
+++ b/module/templates.js
@@ -3,33 +3,34 @@
* Pre-loaded templates are compiled and cached for fast access when rendering
* @return {Promise}
*/
-export const preloadHandlebarsTemplates = async function () {
- return loadTemplates([
- // Shared Partials
- "systems/sw5e/templates/actors/parts/active-effects.html",
+export const preloadHandlebarsTemplates = async function() {
+ return loadTemplates([
- // 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",
+ // Shared Partials
+ "systems/sw5e/templates/actors/parts/active-effects.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",
+ // 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",
- // 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"
- ]);
+ "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"
+ ]);
};
diff --git a/module/token.js b/module/token.js
index 873372cc..db811603 100644
--- a/module/token.js
+++ b/module/token.js
@@ -3,10 +3,11 @@
* @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);
}
@@ -14,16 +15,19 @@ 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);
}
@@ -37,6 +41,7 @@ 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);
@@ -53,50 +58,42 @@ export class Token5e extends Token {
// Determine colors to use
const blk = 0x000000;
- const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
+ const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
- let h = Math.max(canvas.dimensions.size / 12, 8);
- if (this.data.height >= 2) h *= 1.6;
+ let h = Math.max((canvas.dimensions.size / 12), 8);
+ if ( this.data.height >= 2 ) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
- const bs1 = bs + 1;
+ const bs1 = bs+1;
// Overall bar container
- bar.clear();
+ bar.clear()
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
- bar.beginFill(c.tempmax, 1.0)
- .lineStyle(1, blk, 1.0)
- .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
+ bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
- bar.beginFill(c.negmax, 1.0)
- .lineStyle(1, blk, 1.0)
- .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
+ bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
}
// Health bar
- bar.beginFill(hpColor, 1.0)
- .lineStyle(bs, blk, 1.0)
- .drawRoundedRect(0, 0, valuePct * w, h, 2);
+ bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2)
// Temporary hit points
- if (temp > 0) {
- bar.beginFill(c.temp, 1.0)
- .lineStyle(0)
- .drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
+ if ( temp > 0 ) {
+ bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
}
// Set position
- let posY = number === 0 ? this.h - h : 0;
+ let posY = (number === 0) ? (this.h - h) : 0;
bar.position.set(0, posY);
}
}
diff --git a/package-lock.json b/package-lock.json
index b2fbf9ac..759e9b50 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1266,9 +1266,9 @@
}
},
"hosted-git-info": {
- "version": "2.8.9",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
- "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
},
"image-size": {
"version": "0.5.5",
@@ -3068,9 +3068,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
- "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
},
"yargs": {
"version": "7.1.1",
diff --git a/sw5e.js b/sw5e.js
index 3a1f6439..5bef2a6f 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,137 +46,122 @@ import * as migrations from "./module/migration.js";
/* Foundry VTT Initialization */
/* -------------------------------------------- */
-Hooks.once("init", function () {
- console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
+// Keep on while migrating to Foundry version 0.8
+CONFIG.debug.hooks = true;
- // 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
- };
+Hooks.once("init", function() {
+ console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
- // 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"];
+ // 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
+ };
- CONFIG.Dice.DamageRoll = dice.DamageRoll;
- CONFIG.Dice.D20Roll = dice.D20Roll;
+ // 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"
+ ];
- // 5e cone RAW should be 53.13 degrees
- CONFIG.MeasuredTemplate.defaults.angle = 53.13;
+ CONFIG.Dice.DamageRoll = dice.DamageRoll;
+ CONFIG.Dice.D20Roll = dice.D20Roll;
- // Add DND5e namespace for module compatability
- game.dnd5e = game.sw5e;
- CONFIG.DND5E = CONFIG.SW5E;
+ // 5e cone RAW should be 53.13 degrees
+ CONFIG.MeasuredTemplate.defaults.angle = 53.13;
- // Register System Settings
- registerSystemSettings();
+ // Add DND5e namespace for module compatability
+ game.dnd5e = game.sw5e;
+ CONFIG.DND5E = CONFIG.SW5E;
- // Patch Core Functions
- CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
- Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
+ // Register System Settings
+ registerSystemSettings();
- // Register Roll Extensions
- CONFIG.Dice.rolls.push(dice.D20Roll);
- CONFIG.Dice.rolls.push(dice.DamageRoll);
+ // Patch Core Functions
+ CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
+ Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
- // 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 Roll Extensions
+ CONFIG.Dice.rolls.push(dice.D20Roll);
+ CONFIG.Dice.rolls.push(dice.DamageRoll);
- // Preload Handlebars Templates
- return preloadHandlebarsTemplates();
+ // 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();
});
+
/* -------------------------------------------- */
/* Foundry VTT Setup */
/* -------------------------------------------- */
@@ -184,175 +169,131 @@ 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 () {
- // 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"
- ];
+Hooks.once("setup", function() {
- // Exclude some from sorting where the default order matters
- const noSort = [
- "abilities",
- "alignments",
- "currencies",
- "distanceUnits",
- "movementUnits",
- "itemActionTypes",
- "proficiencyLevels",
- "limitedUsePeriods",
- "powerComponents",
- "powerLevels",
- "powerPreparationModes",
- "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"
+ ];
- // 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);
+ // 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);
});
/* -------------------------------------------- */
/**
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
*/
-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));
+Hooks.once("ready", function() {
- // 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;
+ // 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));
- // 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();
+ // 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();
});
/* -------------------------------------------- */
/* 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);
- // Highlight critical success or failure die
- chat.highlightCriticalSuccessFailure(app, html, data);
+ // Display action buttons
+ chat.displayChatActionButtons(app, html, data);
- // Optionally collapse the content
- if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide();
+ // 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();
});
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);
- });
-}
+ 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