Compare commits

...
Sign in to create a new pull request.

20 commits

Author SHA1 Message Date
CK
2212e3c7d8
Merge pull request #239 from unrealkakeman89/Develop
Develop
2021-07-07 09:24:23 -04:00
CK
8c0ad582f7
Merge pull request #238 from unrealkakeman89/prettier
Prettier setup
2021-07-07 09:22:51 -04:00
TJ
584767b352 Formatted js files 2021-07-06 19:57:18 -05:00
TJ
d1b123100e Merge branch 'Develop' into prettier 2021-07-06 19:37:55 -05:00
Jacob Lucas
0a9c9f8ef0 Removed debug flag that spammed the console 2021-07-07 00:40:00 +01:00
CK
ee7418f552
Merge pull request #236 from unrealkakeman89/Develop
Develop
2021-07-06 06:10:24 -04:00
CK
7214e3d260
Merge pull request #234 from unrealkakeman89/Develop
Various powercasting bugs
2021-07-02 20:46:59 -04:00
CK
fc09308d11
Merge pull request #233 from unrealkakeman89/Develop
Develop
2021-07-01 21:54:52 -04:00
CK
5d879f99e2
Merge pull request #232 from unrealkakeman89/Develop
Develop
2021-07-01 16:31:37 -04:00
TJ
da5223cab8 Prettier settings 2021-06-28 18:54:49 -05:00
CK
84ab6cf478
Merge pull request #203 from unrealkakeman89/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-05-12 08:52:14 -04:00
dependabot[bot]
d39fa6acf2
Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 22:42:24 +00:00
CK
0a4b6de0fa
Merge pull request #202 from unrealkakeman89/hotfix-ammo-usage
Fix for blasters not consuming ammo
2021-05-09 19:58:43 -04:00
TJ
c33982f97c Fix for blasters not consuming ammo 2021-05-09 18:53:05 -05:00
CK
4f0c6addcf
Merge pull request #185 from unrealkakeman89/dependabot/npm_and_yarn/y18n-3.2.2
Bump y18n from 3.2.1 to 3.2.2
2021-04-01 11:42:36 -04:00
dependabot[bot]
7c03cd4b04
Bump y18n from 3.2.1 to 3.2.2
Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-31 17:09:05 +00:00
CK
fee77e2172
Merge pull request #172 from unrealkakeman89/Develop
Develop
2021-03-16 10:15:16 -04:00
CK
87d615babc
Merge pull request #155 from ExileofBrokenSky/ExileOfBrokenSky
updated operative
2021-03-11 15:00:27 -05:00
Michael Burgess
04dcaf332d Added Guardian archetype: Aqinos Form
updated icons for scholar and made individual entries for discoveries and maneuvers
added (Maneuver) to fighter and scholar maneuver names, for ease of searching all maneuvers in the directory
2021-03-05 13:09:52 -05:00
Michael Burgess
4ee235566d updated operative
expanded options into class features for each
exploit including skills exploits
visited your mom
individualized the previously shared operative and monk ability score increases
added midi-qol superSaver effect to Evasion (Operative)
2021-02-25 10:40:41 -05:00
44 changed files with 13471 additions and 12713 deletions

14
.prettierrc Normal file
View file

@ -0,0 +1,14 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": false,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View file

@ -1,2 +1,3 @@
{ {
"editor.formatOnSave": true
} }

View file

@ -2,7 +2,7 @@ import { d20Roll, damageRoll } from "../dice.js";
import SelectItemsPrompt from "../apps/select-items-prompt.js"; import SelectItemsPrompt from "../apps/select-items-prompt.js";
import ShortRestDialog from "../apps/short-rest.js"; import ShortRestDialog from "../apps/short-rest.js";
import LongRestDialog from "../apps/long-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"; import Item5e from "../item/entity.js";
/** /**
@ -10,7 +10,6 @@ import Item5e from "../item/entity.js";
* @extends {Actor} * @extends {Actor}
*/ */
export default class Actor5e extends Actor { export default class Actor5e extends Actor {
/** /**
* The data source for Actor5e.classes allowing it to be lazily computed. * The data source for Actor5e.classes allowing it to be lazily computed.
* @type {Object<string, Item5e>} * @type {Object<string, Item5e>}
@ -28,11 +27,13 @@ export default class Actor5e extends Actor {
*/ */
get classes() { get classes() {
if (this._classes !== undefined) return this._classes; if (this._classes !== undefined) return this._classes;
if ( this.data.type !== "character" ) return this._classes = {}; if (this.data.type !== "character") return (this._classes = {});
return this._classes = this.items.filter((item) => item.type === "class").reduce((obj, cls) => { return (this._classes = this.items
.filter((item) => item.type === "class")
.reduce((obj, cls) => {
obj[cls.name.slugify({strict: true})] = cls; obj[cls.name.slugify({strict: true})] = cls;
return obj; return obj;
}, {}); }, {}));
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -54,7 +55,7 @@ export default class Actor5e extends Actor {
super.prepareData(); super.prepareData();
// iterate over owned items and recompute attributes that depend on prepared actor data // iterate over owned items and recompute attributes that depend on prepared actor data
this.items.forEach(item => item.prepareFinalAttributes()); this.items.forEach((item) => item.prepareFinalAttributes());
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -86,8 +87,8 @@ export default class Actor5e extends Actor {
let originalSaves = null; let originalSaves = null;
let originalSkills = null; let originalSkills = null;
if (this.isPolymorphed) { if (this.isPolymorphed) {
const transformOptions = this.getFlag('sw5e', 'transformOptions'); const transformOptions = this.getFlag("sw5e", "transformOptions");
const original = game.actors?.get(this.getFlag('sw5e', 'originalActor')); const original = game.actors?.get(this.getFlag("sw5e", "originalActor"));
if (original) { if (original) {
if (transformOptions.mergeSaves) { if (transformOptions.mergeSaves) {
originalSaves = original.data.data.abilities; originalSaves = original.data.data.abilities;
@ -202,13 +203,13 @@ export default class Actor5e extends Actor {
let toCreate = []; let toCreate = [];
if (prompt) { if (prompt) {
const itemIdsToAdd = await SelectItemsPrompt.create(items, { const itemIdsToAdd = await SelectItemsPrompt.create(items, {
hint: game.i18n.localize('SW5E.AddEmbeddedItemPromptHint') hint: game.i18n.localize("SW5E.AddEmbeddedItemPromptHint")
}); });
for (let item of items) { for (let item of items) {
if (itemIdsToAdd.includes(item.id)) toCreate.push(item.toObject()); if (itemIdsToAdd.includes(item.id)) toCreate.push(item.toObject());
} }
} else { } else {
toCreate = items.map(item => item.toObject()); toCreate = items.map((item) => item.toObject());
} }
// Create the requested items // Create the requested items
@ -223,9 +224,9 @@ export default class Actor5e extends Actor {
* Optionally prompt the user for which they would like to add. * Optionally prompt the user for which they would like to add.
*/ */
async getClassFeatures({className, archetypeName, level} = {}) { async getClassFeatures({className, archetypeName, level} = {}) {
const existing = new Set(this.items.map(i => i.name)); const existing = new Set(this.items.map((i) => i.name));
const features = await Actor5e.loadClassFeatures({className, archetypeName, level}); const features = await Actor5e.loadClassFeatures({className, archetypeName, level});
return features.filter(f => !existing.has(f.name)) || []; return features.filter((f) => !existing.has(f.name)) || [];
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -250,14 +251,14 @@ export default class Actor5e extends Actor {
let ids = []; let ids = [];
for (let [l, f] of Object.entries(clsConfig.features || {})) { for (let [l, f] of Object.entries(clsConfig.features || {})) {
l = parseInt(l); l = parseInt(l);
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f); if (l <= level && l > priorLevel) ids = ids.concat(f);
} }
// Acquire archetype features // Acquire archetype features
const archConfig = clsConfig.archetypes[archetypeName] || {}; const archConfig = clsConfig.archetypes[archetypeName] || {};
for (let [l, f] of Object.entries(archConfig.features || {})) { for (let [l, f] of Object.entries(archConfig.features || {})) {
l = parseInt(l); l = parseInt(l);
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f); if (l <= level && l > priorLevel) ids = ids.concat(f);
} }
// Load item data for all identified features // Load item data for all identified features
@ -288,14 +289,17 @@ export default class Actor5e extends Actor {
const data = actorData.data; const data = actorData.data;
// Determine character level and available hit dice based on owned Class items // Determine character level and available hit dice based on owned Class items
const [level, hd] = this.items.reduce((arr, item) => { const [level, hd] = this.items.reduce(
(arr, item) => {
if (item.type === "class") { if (item.type === "class") {
const classLevels = parseInt(item.data.data.levels) || 1; const classLevels = parseInt(item.data.data.levels) || 1;
arr[0] += classLevels; arr[0] += classLevels;
arr[1] += classLevels - (parseInt(item.data.data.hitDiceUsed) || 0); arr[1] += classLevels - (parseInt(item.data.data.hitDiceUsed) || 0);
} }
return arr; return arr;
}, [0, 0]); },
[0, 0]
);
data.details.level = level; data.details.level = level;
data.attributes.hd = hd; data.attributes.hd = hd;
@ -307,7 +311,7 @@ export default class Actor5e extends Actor {
xp.max = this.getLevelExp(level || 1); xp.max = this.getLevelExp(level || 1);
const prior = this.getLevelExp(level - 1 || 0); const prior = this.getLevelExp(level - 1 || 0);
const required = xp.max - prior; const required = xp.max - prior;
const pct = Math.round((xp.value - prior) * 100 / required); const pct = Math.round(((xp.value - prior) * 100) / required);
xp.pct = Math.clamped(pct, 0, 100); xp.pct = Math.clamped(pct, 0, 100);
// Add base Powercasting attributes // Add base Powercasting attributes
@ -376,7 +380,7 @@ export default class Actor5e extends Actor {
* @private * @private
*/ */
_prepareSkills(actorData, bonuses, checkBonus, originalSkills) { _prepareSkills(actorData, bonuses, checkBonus, originalSkills) {
if (actorData.type === 'vehicle') return; if (actorData.type === "vehicle") return;
const data = actorData.data; const data = actorData.data;
const flags = actorData.flags.sw5e || {}; const flags = actorData.flags.sw5e || {};
@ -392,13 +396,13 @@ export default class Actor5e extends Actor {
let round = Math.floor; let round = Math.floor;
// Remarkable // Remarkable
if ( athlete && (skl.value < 0.5) && feats.remarkableAthlete.abilities.includes(skl.ability) ) { if (athlete && skl.value < 0.5 && feats.remarkableAthlete.abilities.includes(skl.ability)) {
skl.value = 0.5; skl.value = 0.5;
round = Math.ceil; round = Math.ceil;
} }
// Jack of All Trades // Jack of All Trades
if ( joat && (skl.value < 0.5) ) { if (joat && skl.value < 0.5) {
skl.value = 0.5; skl.value = 0.5;
} }
@ -414,7 +418,7 @@ export default class Actor5e extends Actor {
skl.total = skl.mod + skl.prof + skl.bonus; skl.total = skl.mod + skl.prof + skl.bonus;
// Compute passive bonus // Compute passive bonus
const passive = observant && (feats.observantFeat.skills.includes(id)) ? 5 : 0; const passive = observant && feats.observantFeat.skills.includes(id) ? 5 : 0;
skl.passive = 10 + skl.total + passive; skl.passive = 10 + skl.total + passive;
} }
} }
@ -426,10 +430,10 @@ export default class Actor5e extends Actor {
* @private * @private
*/ */
_computeBasePowercasting(actorData) { _computeBasePowercasting(actorData) {
if (actorData.type === 'vehicle' || actorData.type === 'starship') return; if (actorData.type === "vehicle" || actorData.type === "starship") return;
const ad = actorData.data; const ad = actorData.data;
const powers = ad.powers; const powers = ad.powers;
const isNPC = actorData.type === 'npc'; const isNPC = actorData.type === "npc";
// Translate the list of classes into force and tech power-casting progression // Translate the list of classes into force and tech power-casting progression
const forceProgression = { const forceProgression = {
@ -456,7 +460,7 @@ export default class Actor5e extends Actor {
}; };
// Tabulate the total power-casting progression // Tabulate the total power-casting progression
const classes = this.data.items.filter(i => i.type === "class"); const classes = this.data.items.filter((i) => i.type === "class");
let priority = 0; let priority = 0;
for (let cls of classes) { for (let cls of classes) {
const d = cls.data.data; const d = cls.data.data;
@ -465,82 +469,88 @@ export default class Actor5e extends Actor {
const prog = d.powercasting.progression; const prog = d.powercasting.progression;
// TODO: Consider a more dynamic system // TODO: Consider a more dynamic system
switch (prog) { switch (prog) {
case 'consular': case "consular":
priority = 3; priority = 3;
forceProgression.levels += levels; forceProgression.levels += levels;
forceProgression.multi += (SW5E.powerMaxLevel['consular'][19]/9)*levels; forceProgression.multi += (SW5E.powerMaxLevel["consular"][19] / 9) * levels;
forceProgression.classes++; forceProgression.classes++;
// see if class controls high level forcecasting // see if class controls high level forcecasting
if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
forceProgression.maxClass = 'consular'; forceProgression.maxClass = "consular";
forceProgression.maxClassLevels = levels; forceProgression.maxClassLevels = levels;
forceProgression.maxClassPriority = priority; forceProgression.maxClassPriority = priority;
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['consular'][Math.clamped((levels - 1), 0, 20)]; forceProgression.maxClassPowerLevel =
SW5E.powerMaxLevel["consular"][Math.clamped(levels - 1, 0, 20)];
} }
// calculate points and powers known // calculate points and powers known
forceProgression.powersKnown += SW5E.powersKnown['consular'][Math.clamped((levels - 1), 0, 20)]; forceProgression.powersKnown += SW5E.powersKnown["consular"][Math.clamped(levels - 1, 0, 20)];
forceProgression.points += SW5E.powerPoints['consular'][Math.clamped((levels - 1), 0, 20)]; forceProgression.points += SW5E.powerPoints["consular"][Math.clamped(levels - 1, 0, 20)];
break; break;
case 'engineer': case "engineer":
priority = 2 priority = 2;
techProgression.levels += levels; techProgression.levels += levels;
techProgression.multi += (SW5E.powerMaxLevel['engineer'][19]/9)*levels; techProgression.multi += (SW5E.powerMaxLevel["engineer"][19] / 9) * levels;
techProgression.classes++; techProgression.classes++;
// see if class controls high level techcasting // see if class controls high level techcasting
if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){ if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) {
techProgression.maxClass = 'engineer'; techProgression.maxClass = "engineer";
techProgression.maxClassLevels = levels; techProgression.maxClassLevels = levels;
techProgression.maxClassPriority = priority; techProgression.maxClassPriority = priority;
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['engineer'][Math.clamped((levels - 1), 0, 20)]; techProgression.maxClassPowerLevel =
SW5E.powerMaxLevel["engineer"][Math.clamped(levels - 1, 0, 20)];
} }
techProgression.powersKnown += SW5E.powersKnown['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)]; techProgression.points += SW5E.powerPoints["engineer"][Math.clamped(levels - 1, 0, 20)];
break; break;
case 'guardian': case "guardian":
priority = 1; priority = 1;
forceProgression.levels += levels; forceProgression.levels += levels;
forceProgression.multi += (SW5E.powerMaxLevel['guardian'][19]/9)*levels; forceProgression.multi += (SW5E.powerMaxLevel["guardian"][19] / 9) * levels;
forceProgression.classes++; forceProgression.classes++;
// see if class controls high level forcecasting // see if class controls high level forcecasting
if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
forceProgression.maxClass = 'guardian'; forceProgression.maxClass = "guardian";
forceProgression.maxClassLevels = levels; forceProgression.maxClassLevels = levels;
forceProgression.maxClassPriority = priority; forceProgression.maxClassPriority = priority;
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['guardian'][Math.clamped((levels - 1), 0, 20)]; forceProgression.maxClassPowerLevel =
SW5E.powerMaxLevel["guardian"][Math.clamped(levels - 1, 0, 20)];
} }
forceProgression.powersKnown += SW5E.powersKnown['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)]; forceProgression.points += SW5E.powerPoints["guardian"][Math.clamped(levels - 1, 0, 20)];
break; break;
case 'scout': case "scout":
priority = 1; priority = 1;
techProgression.levels += levels; techProgression.levels += levels;
techProgression.multi += (SW5E.powerMaxLevel['scout'][19]/9)*levels; techProgression.multi += (SW5E.powerMaxLevel["scout"][19] / 9) * levels;
techProgression.classes++; techProgression.classes++;
// see if class controls high level techcasting // see if class controls high level techcasting
if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){ if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) {
techProgression.maxClass = 'scout'; techProgression.maxClass = "scout";
techProgression.maxClassLevels = levels; techProgression.maxClassLevels = levels;
techProgression.maxClassPriority = priority; techProgression.maxClassPriority = priority;
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['scout'][Math.clamped((levels - 1), 0, 20)]; techProgression.maxClassPowerLevel =
SW5E.powerMaxLevel["scout"][Math.clamped(levels - 1, 0, 20)];
} }
techProgression.powersKnown += SW5E.powersKnown['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)]; techProgression.points += SW5E.powerPoints["scout"][Math.clamped(levels - 1, 0, 20)];
break; break;
case 'sentinel': case "sentinel":
priority = 2; priority = 2;
forceProgression.levels += levels; forceProgression.levels += levels;
forceProgression.multi += (SW5E.powerMaxLevel['sentinel'][19]/9)*levels; forceProgression.multi += (SW5E.powerMaxLevel["sentinel"][19] / 9) * levels;
forceProgression.classes++; forceProgression.classes++;
// see if class controls high level forcecasting // see if class controls high level forcecasting
if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){ if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) {
forceProgression.maxClass = 'sentinel'; forceProgression.maxClass = "sentinel";
forceProgression.maxClassLevels = levels; forceProgression.maxClassLevels = levels;
forceProgression.maxClassPriority = priority; forceProgression.maxClassPriority = priority;
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['sentinel'][Math.clamped((levels - 1), 0, 20)]; 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;
} }
forceProgression.powersKnown += SW5E.powersKnown['sentinel'][Math.clamped((levels - 1), 0, 20)];
forceProgression.points += SW5E.powerPoints['sentinel'][Math.clamped((levels - 1), 0, 20)];
break; }
} }
if (isNPC) { if (isNPC) {
@ -549,30 +559,31 @@ export default class Actor5e extends Actor {
forceProgression.levels = ad.details.powerForceLevel; forceProgression.levels = ad.details.powerForceLevel;
ad.attributes.force.level = forceProgression.levels; ad.attributes.force.level = forceProgression.levels;
forceProgression.maxClass = ad.attributes.powercasting; forceProgression.maxClass = ad.attributes.powercasting;
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped((forceProgression.levels - 1), 0, 20)]; forceProgression.maxClassPowerLevel =
SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped(forceProgression.levels - 1, 0, 20)];
} }
if (ad.details.powerTechLevel) { if (ad.details.powerTechLevel) {
techProgression.levels = ad.details.powerTechLevel; techProgression.levels = ad.details.powerTechLevel;
ad.attributes.tech.level = techProgression.levels; ad.attributes.tech.level = techProgression.levels;
techProgression.maxClass = ad.attributes.powercasting; techProgression.maxClass = ad.attributes.powercasting;
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped((techProgression.levels - 1), 0, 20)]; techProgression.maxClassPowerLevel =
SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped(techProgression.levels - 1, 0, 20)];
} }
} else { } else {
// EXCEPTION: multi-classed progression uses multi rounded down rather than levels // EXCEPTION: multi-classed progression uses multi rounded down rather than levels
if (forceProgression.classes > 1) { if (forceProgression.classes > 1) {
forceProgression.levels = Math.floor(forceProgression.multi); forceProgression.levels = Math.floor(forceProgression.multi);
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][forceProgression.levels - 1]; forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][forceProgression.levels - 1];
} }
if (techProgression.classes > 1) { if (techProgression.classes > 1) {
techProgression.levels = Math.floor(techProgression.multi); techProgression.levels = Math.floor(techProgression.multi);
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][techProgression.levels - 1]; techProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][techProgression.levels - 1];
} }
} }
// Look up the number of slots per level from the powerLimit table // Look up the number of slots per level from the powerLimit table
let forcePowerLimit = Array.from(SW5E.powerLimit['none']); let forcePowerLimit = Array.from(SW5E.powerLimit["none"]);
for (let i = 0; i < (forceProgression.maxClassPowerLevel); i++) { for (let i = 0; i < forceProgression.maxClassPowerLevel; i++) {
forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i]; forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i];
} }
@ -588,8 +599,8 @@ export default class Actor5e extends Actor {
} }
} }
let techPowerLimit = Array.from(SW5E.powerLimit['none']); let techPowerLimit = Array.from(SW5E.powerLimit["none"]);
for (let i = 0; i < (techProgression.maxClassPowerLevel); i++) { for (let i = 0; i < techProgression.maxClassPowerLevel; i++) {
techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i]; techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i];
} }
@ -619,11 +630,10 @@ export default class Actor5e extends Actor {
} }
} }
// Tally Powers Known and check for migration first to avoid errors // Tally Powers Known and check for migration first to avoid errors
let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined; let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined;
if (hasKnownPowers) { if (hasKnownPowers) {
const knownPowers = this.data.items.filter(i => i.type === "power"); const knownPowers = this.data.items.filter((i) => i.type === "power");
let knownForcePowers = 0; let knownForcePowers = 0;
let knownTechPowers = 0; let knownTechPowers = 0;
for (let knownPower of knownPowers) { for (let knownPower of knownPowers) {
@ -653,8 +663,7 @@ export default class Actor5e extends Actor {
* @private * @private
*/ */
_computeDerivedPowercasting(actorData) { _computeDerivedPowercasting(actorData) {
if (!(actorData.type === "character" || actorData.type === "npc")) return;
if (!(actorData.type === 'character' || actorData.type === 'npc')) return;
const ad = actorData.data; const ad = actorData.data;
@ -662,10 +671,11 @@ export default class Actor5e extends Actor {
// TODO: Consider an option for using the variant rule of all powers use the same value // 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.powerForceLightDC = 8 + ad.abilities.wis.mod + ad.attributes.prof ?? 10;
ad.attributes.powerForceDarkDC = 8 + ad.abilities.cha.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.powerForceUnivDC =
Math.max(ad.attributes.powerForceLightDC, ad.attributes.powerForceDarkDC) ?? 10;
ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10; ad.attributes.powerTechDC = 8 + ad.abilities.int.mod + ad.attributes.prof ?? 10;
if (actorData.type !== 'character') return; if (actorData.type !== "character") return;
// Set Force and tech bonus points for PC Actors // Set Force and tech bonus points for PC Actors
if (!!ad.attributes.force.level) { if (!!ad.attributes.force.level) {
@ -674,7 +684,6 @@ export default class Actor5e extends Actor {
if (!!ad.attributes.tech.level) { if (!!ad.attributes.tech.level) {
ad.attributes.tech.points.max += ad.abilities.int.mod; ad.attributes.tech.points.max += ad.abilities.int.mod;
} }
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -696,18 +705,19 @@ export default class Actor5e extends Actor {
if (!physicalItems.includes(i.type)) return weight; if (!physicalItems.includes(i.type)) return weight;
const q = i.data.data.quantity || 0; const q = i.data.data.quantity || 0;
const w = i.data.data.weight || 0; const w = i.data.data.weight || 0;
return weight + (q * w); return weight + q * w;
}, 0); }, 0);
// [Optional] add Currency Weight (for non-transformed actors) // [Optional] add Currency Weight (for non-transformed actors)
if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) { if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) {
const currency = actorData.data.currency; const currency = actorData.data.currency;
const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0); const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0);
weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
} }
// Determine the encumbrance size class // Determine the encumbrance size class
let mod = { let mod =
{
tiny: 0.5, tiny: 0.5,
sm: 1, sm: 1,
med: 1, med: 1,
@ -721,7 +731,7 @@ export default class Actor5e extends Actor {
weight = weight.toNearest(0.1); weight = weight.toNearest(0.1);
const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod;
const pct = Math.clamped((weight * 100) / max, 0, 100); const pct = Math.clamped((weight * 100) / max, 0, 100);
return { value: weight.toNearest(0.1), max, pct, encumbered: pct > (2/3) }; return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3};
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -750,7 +760,7 @@ export default class Actor5e extends Actor {
// Apply changes in Actor size to Token width/height // Apply changes in Actor size to Token width/height
const newSize = foundry.utils.getProperty(changed, "data.traits.size"); const newSize = foundry.utils.getProperty(changed, "data.traits.size");
if ( newSize && (newSize !== foundry.utils.getProperty(this.data, "data.traits.size")) ) { if (newSize && newSize !== foundry.utils.getProperty(this.data, "data.traits.size")) {
let size = CONFIG.SW5E.tokenSizes[newSize]; let size = CONFIG.SW5E.tokenSizes[newSize];
if (!foundry.utils.hasProperty(changed, "token.width")) { if (!foundry.utils.hasProperty(changed, "token.width")) {
changed.token = changed.token || {}; changed.token = changed.token || {};
@ -761,7 +771,7 @@ export default class Actor5e extends Actor {
// Reset death save counters // Reset death save counters
const isDead = this.data.data.attributes.hp.value <= 0; const isDead = this.data.data.attributes.hp.value <= 0;
if ( isDead && (foundry.utils.getProperty(changed, "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.success", 0);
foundry.utils.setProperty(changed, "data.attributes.death.failure", 0); foundry.utils.setProperty(changed, "data.attributes.death.failure", 0);
} }
@ -787,7 +797,7 @@ export default class Actor5e extends Actor {
async modifyTokenAttribute(attribute, value, isDelta, isBar) { async modifyTokenAttribute(attribute, value, isDelta, isBar) {
if (attribute === "attributes.hp") { if (attribute === "attributes.hp") {
const hp = getProperty(this.data.data, attribute); const hp = getProperty(this.data.data, attribute);
const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value; const delta = isDelta ? -1 * value : hp.value + hp.temp - value;
return this.applyDamage(delta); return this.applyDamage(delta);
} }
return super.modifyTokenAttribute(attribute, value, isDelta, isBar); return super.modifyTokenAttribute(attribute, value, isDelta, isBar);
@ -821,12 +831,16 @@ export default class Actor5e extends Actor {
// Delegate damage application to a hook // Delegate damage application to a hook
// TODO replace this in the future with a better modifyTokenAttribute function in the core // TODO replace this in the future with a better modifyTokenAttribute function in the core
const allowed = Hooks.call("modifyTokenAttribute", { const allowed = Hooks.call(
"modifyTokenAttribute",
{
attribute: "attributes.hp", attribute: "attributes.hp",
value: amount, value: amount,
isDelta: false, isDelta: false,
isBar: true isBar: true
}, updates); },
updates
);
return allowed !== false ? this.update(updates) : this; return allowed !== false ? this.update(updates) : this;
} }
@ -865,17 +879,19 @@ export default class Actor5e extends Actor {
} }
// Reliable Talent applies to any skill check we have full or better proficiency in // Reliable Talent applies to any skill check we have full or better proficiency in
const reliableTalent = (skl.value >= 1 && this.getFlag("sw5e", "reliableTalent")); const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent");
// Roll and return // Roll and return
const rollData = foundry.utils.mergeObject(options, { const rollData = foundry.utils.mergeObject(options, {
parts: parts, parts: parts,
data: data, data: data,
title: game.i18n.format("SW5E.SkillPromptTitle", {skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]}), title: game.i18n.format("SW5E.SkillPromptTitle", {
skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId]
}),
halflingLucky: this.getFlag("sw5e", "halflingLucky"), halflingLucky: this.getFlag("sw5e", "halflingLucky"),
reliableTalent: reliableTalent, reliableTalent: reliableTalent,
messageData: { messageData: {
speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
"flags.sw5e.roll": {type: "skill", skillId} "flags.sw5e.roll": {type: "skill", skillId}
} }
}); });
@ -930,8 +946,7 @@ export default class Actor5e extends Actor {
if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) { if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) {
parts.push("@proficiency"); parts.push("@proficiency");
data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof); data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof);
} } else if (feats.jackOfAllTrades) {
else if ( feats.jackOfAllTrades ) {
parts.push("@proficiency"); parts.push("@proficiency");
data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof); data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof);
} }
@ -955,7 +970,7 @@ export default class Actor5e extends Actor {
title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
halflingLucky: feats.halflingLucky, halflingLucky: feats.halflingLucky,
messageData: { messageData: {
speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
"flags.sw5e.roll": {type: "ability", abilityId} "flags.sw5e.roll": {type: "ability", abilityId}
} }
}); });
@ -1004,7 +1019,7 @@ export default class Actor5e extends Actor {
title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}), title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}),
halflingLucky: this.getFlag("sw5e", "halflingLucky"), halflingLucky: this.getFlag("sw5e", "halflingLucky"),
messageData: { messageData: {
speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
"flags.sw5e.roll": {type: "save", abilityId} "flags.sw5e.roll": {type: "save", abilityId}
} }
}); });
@ -1019,10 +1034,9 @@ export default class Actor5e extends Actor {
* @return {Promise<Roll|null>} A Promise which resolves to the Roll instance * @return {Promise<Roll|null>} A Promise which resolves to the Roll instance
*/ */
async rollDeathSave(options = {}) { async rollDeathSave(options = {}) {
// Display a warning if we are not at zero HP or if we already have reached 3 // Display a warning if we are not at zero HP or if we already have reached 3
const death = this.data.data.attributes.death; const death = this.data.data.attributes.death;
if ( (this.data.data.attributes.hp.value > 0) || (death.failure >= 3) || (death.success >= 3)) { if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) {
ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary")); ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary"));
return null; return null;
} }
@ -1046,7 +1060,7 @@ export default class Actor5e extends Actor {
halflingLucky: this.getFlag("sw5e", "halflingLucky"), halflingLucky: this.getFlag("sw5e", "halflingLucky"),
targetValue: 10, targetValue: 10,
messageData: { messageData: {
speaker: options.speaker || ChatMessage.getSpeaker({actor: this}), "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}),
"flags.sw5e.roll": {type: "death"} "flags.sw5e.roll": {type: "death"}
} }
}); });
@ -1090,7 +1104,8 @@ export default class Actor5e extends Actor {
else { else {
let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1); let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1);
await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)}); await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)});
if ( failures >= 3 ) { // 3 Failures = death if (failures >= 3) {
// 3 Failures = death
chatString = "SW5E.DeathSaveFailure"; chatString = "SW5E.DeathSaveFailure";
} }
} }
@ -1116,20 +1131,19 @@ export default class Actor5e extends Actor {
* @return {Promise<Roll|null>} The created Roll instance, or null if no hit die was rolled * @return {Promise<Roll|null>} The created Roll instance, or null if no hit die was rolled
*/ */
async rollHitDie(denomination, {dialog = true} = {}) { async rollHitDie(denomination, {dialog = true} = {}) {
// If no denomination was provided, choose the first available // If no denomination was provided, choose the first available
let cls = null; let cls = null;
if (!denomination) { if (!denomination) {
cls = this.itemTypes.class.find(c => c.data.data.hitDiceUsed < c.data.data.levels); cls = this.itemTypes.class.find((c) => c.data.data.hitDiceUsed < c.data.data.levels);
if (!cls) return null; if (!cls) return null;
denomination = cls.data.data.hitDice; denomination = cls.data.data.hitDice;
} }
// Otherwise locate a class (if any) which has an available hit die of the requested denomination // Otherwise locate a class (if any) which has an available hit die of the requested denomination
else { else {
cls = this.items.find(i => { cls = this.items.find((i) => {
const d = i.data.data; const d = i.data.data;
return (d.hitDice === denomination) && ((d.hitDiceUsed || 0) < (d.levels || 1)); return d.hitDice === denomination && (d.hitDiceUsed || 0) < (d.levels || 1);
}); });
} }
@ -1154,7 +1168,7 @@ export default class Actor5e extends Actor {
fastForward: !dialog, fastForward: !dialog,
dialogOptions: {width: 350}, dialogOptions: {width: 350},
messageData: { messageData: {
speaker: ChatMessage.getSpeaker({actor: this}), "speaker": ChatMessage.getSpeaker({actor: this}),
"flags.sw5e.roll": {type: "hitDie"} "flags.sw5e.roll": {type: "hitDie"}
} }
}); });
@ -1195,7 +1209,6 @@ export default class Actor5e extends Actor {
* @return {Promise.<RestResult>} A Promise which resolves once the short rest workflow has completed. * @return {Promise.<RestResult>} A Promise which resolves once the short rest workflow has completed.
*/ */
async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) { async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) {
// Take note of the initial hit points and number of hit dice the Actor has // Take note of the initial hit points and number of hit dice the Actor has
const hd0 = this.data.data.attributes.hd; const hd0 = this.data.data.attributes.hd;
const hp0 = this.data.data.attributes.hp.value; const hp0 = this.data.data.attributes.hp.value;
@ -1215,7 +1228,14 @@ export default class Actor5e extends Actor {
await this.autoSpendHitDice({threshold: autoHDThreshold}); await this.autoSpendHitDice({threshold: autoHDThreshold});
} }
return this._rest(chat, newDay, false, this.data.data.attributes.hd - hd0, this.data.data.attributes.hp.value - hp0, this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value); return this._rest(
chat,
newDay,
false,
this.data.data.attributes.hd - hd0,
this.data.data.attributes.hp.value - hp0,
this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -1239,7 +1259,15 @@ export default class Actor5e extends Actor {
} }
} }
return this._rest(chat, newDay, true, 0, 0, this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value, this.data.data.attributes.force.points.max - this.data.data.attributes.force.points.value); return this._rest(
chat,
newDay,
true,
0,
0,
this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value,
this.data.data.attributes.force.points.max - this.data.data.attributes.force.points.value
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -1278,7 +1306,10 @@ export default class Actor5e extends Actor {
dfp: dfp, dfp: dfp,
updateData: { updateData: {
...hitPointUpdates, ...hitPointUpdates,
...this._getRestResourceRecovery({ recoverShortRestResources: !longRest, recoverLongRestResources: longRest }), ...this._getRestResourceRecovery({
recoverShortRestResources: !longRest,
recoverLongRestResources: longRest
}),
...this._getRestPowerRecovery({recoverForcePowers: longRest}) ...this._getRestPowerRecovery({recoverForcePowers: longRest})
}, },
updateItems: [ updateItems: [
@ -1286,7 +1317,7 @@ export default class Actor5e extends Actor {
...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay}) ...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay})
], ],
newDay: newDay newDay: newDay
} };
// Perform updates // Perform updates
await this.update(result.updateData); await this.update(result.updateData);
@ -1319,9 +1350,15 @@ export default class Actor5e extends Actor {
// Summarize the rest duration // Summarize the rest duration
switch (game.settings.get("sw5e", "restVariant")) { switch (game.settings.get("sw5e", "restVariant")) {
case 'normal': restFlavor = (longRest && newDay) ? "SW5E.LongRestOvernight" : `SW5E.${length}RestNormal`; break; case "normal":
case 'gritty': restFlavor = (!longRest && newDay) ? "SW5E.ShortRestOvernight" : `SW5E.${length}RestGritty`; break; restFlavor = longRest && newDay ? "SW5E.LongRestOvernight" : `SW5E.${length}RestNormal`;
case 'epic': restFlavor = `SW5E.${length}RestEpic`; break; break;
case "gritty":
restFlavor = !longRest && newDay ? "SW5E.ShortRestOvernight" : `SW5E.${length}RestGritty`;
break;
case "epic":
restFlavor = `SW5E.${length}RestEpic`;
break;
} }
// Determine the chat message to display // Determine the chat message to display
@ -1333,7 +1370,7 @@ export default class Actor5e extends Actor {
if (dhd !== 0) message += "HD"; if (dhd !== 0) message += "HD";
} else { } else {
message = "SW5E.ShortRestResultShort"; message = "SW5E.ShortRestResultShort";
if ((dhd !== 0) && (dhp !== 0)){ if (dhd !== 0 && dhp !== 0) {
if (dtp !== 0) { if (dtp !== 0) {
message = "SW5E.ShortRestResultWithTech"; message = "SW5E.ShortRestResultWithTech";
} else { } else {
@ -1376,7 +1413,7 @@ export default class Actor5e extends Actor {
const max = this.data.data.attributes.hp.max + this.data.data.attributes.hp.tempmax; const max = this.data.data.attributes.hp.max + this.data.data.attributes.hp.tempmax;
let diceRolled = 0; let diceRolled = 0;
while ( (this.data.data.attributes.hp.value + threshold) <= max ) { while (this.data.data.attributes.hp.value + threshold <= max) {
const r = await this.rollHitDie(undefined, {dialog: false}); const r = await this.rollHitDie(undefined, {dialog: false});
if (r === null) break; if (r === null) break;
diceRolled += 1; diceRolled += 1;
@ -1427,7 +1464,10 @@ export default class Actor5e extends Actor {
_getRestResourceRecovery({recoverShortRestResources = true, recoverLongRestResources = true} = {}) { _getRestResourceRecovery({recoverShortRestResources = true, recoverLongRestResources = true} = {}) {
let updates = {}; let updates = {};
for (let [k, r] of Object.entries(this.data.data.resources)) { for (let [k, r] of Object.entries(this.data.data.resources)) {
if ( Number.isNumeric(r.max) && ((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr)) ) { if (
Number.isNumeric(r.max) &&
((recoverShortRestResources && r.sr) || (recoverLongRestResources && r.lr))
) {
updates[`data.resources.${k}.value`] = Number(r.max); updates[`data.resources.${k}.value`] = Number(r.max);
} }
} }
@ -1452,7 +1492,7 @@ export default class Actor5e extends Actor {
updates["data.attributes.tech.points.tempmax"] = 0; updates["data.attributes.tech.points.tempmax"] = 0;
for (let [k, v] of Object.entries(this.data.data.powers)) { for (let [k, v] of Object.entries(this.data.data.powers)) {
updates[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : (v.tmax ?? 0); updates[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : v.tmax ?? 0;
} }
} }
@ -1462,7 +1502,7 @@ export default class Actor5e extends Actor {
updates["data.attributes.force.points.tempmax"] = 0; updates["data.attributes.force.points.tempmax"] = 0;
for (let [k, v] of Object.entries(this.data.data.powers)) { for (let [k, v] of Object.entries(this.data.data.powers)) {
updates[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : (v.fmax ?? 0); updates[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : v.fmax ?? 0;
} }
} }
@ -1494,10 +1534,10 @@ export default class Actor5e extends Actor {
let hitDiceRecovered = 0; let hitDiceRecovered = 0;
for (let item of sortedClasses) { for (let item of sortedClasses) {
const d = item.data.data; const d = item.data.data;
if ( (hitDiceRecovered < maxHitDice) && (d.hitDiceUsed > 0) ) { if (hitDiceRecovered < maxHitDice && d.hitDiceUsed > 0) {
let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered); let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered);
hitDiceRecovered += delta; hitDiceRecovered += delta;
updates.push({_id: item.id, "data.hitDiceUsed": d.hitDiceUsed - delta}); updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta});
} }
} }
@ -1526,10 +1566,10 @@ export default class Actor5e extends Actor {
for (let item of this.items) { for (let item of this.items) {
const d = item.data.data; const d = item.data.data;
if (d.uses && recovery.includes(d.uses.per)) { if (d.uses && recovery.includes(d.uses.per)) {
updates.push({_id: item.id, "data.uses.value": d.uses.max}); updates.push({"_id": item.id, "data.uses.value": d.uses.max});
} }
if (recoverLongRestUses && d.recharge && d.recharge.value) { if (recoverLongRestUses && d.recharge && d.recharge.value) {
updates.push({_id: item.id, "data.recharge.charged": true}); updates.push({"_id": item.id, "data.recharge.charged": true});
} }
} }
@ -1556,10 +1596,24 @@ export default class Actor5e extends Actor {
* @param {boolean} [keepVision] Keep vision * @param {boolean} [keepVision] Keep vision
* @param {boolean} [transformTokens] Transform linked tokens too * @param {boolean} [transformTokens] Transform linked tokens too
*/ */
async transformInto(target, { keepPhysical=false, keepMental=false, keepSaves=false, keepSkills=false, async transformInto(
mergeSaves=false, mergeSkills=false, keepClass=false, keepFeats=false, keepPowers=false, target,
keepItems=false, keepBio=false, keepVision=false, transformTokens=true}={}) { {
keepPhysical = false,
keepMental = false,
keepSaves = false,
keepSkills = false,
mergeSaves = false,
mergeSkills = false,
keepClass = false,
keepFeats = false,
keepPowers = false,
keepItems = false,
keepBio = false,
keepVision = false,
transformTokens = true
} = {}
) {
// Ensure the player is allowed to polymorph // Ensure the player is allowed to polymorph
const allowed = game.settings.get("sw5e", "allowPolymorphing"); const allowed = game.settings.get("sw5e", "allowPolymorphing");
if (!allowed && !game.user.isGM) { if (!allowed && !game.user.isGM) {
@ -1602,7 +1656,7 @@ export default class Actor5e extends Actor {
d.token[k] = source.token[k]; d.token[k] = source.token[k];
} }
if (!keepVision) { if (!keepVision) {
for ( let k of ['dimSight', 'brightSight', 'dimLight', 'brightLight', 'vision', 'sightAngle'] ) { for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) {
d.token[k] = source.token[k]; d.token[k] = source.token[k];
} }
} }
@ -1631,18 +1685,20 @@ export default class Actor5e extends Actor {
} }
// Keep specific items from the original data // Keep specific items from the original data
d.items = d.items.concat(o.items.filter(i => { d.items = d.items.concat(
o.items.filter((i) => {
if (i.type === "class") return keepClass; if (i.type === "class") return keepClass;
else if (i.type === "feat") return keepFeats; else if (i.type === "feat") return keepFeats;
else if (i.type === "power") return keepPowers; else if (i.type === "power") return keepPowers;
else return keepItems; else return keepItems;
})); })
);
// Transfer classes for NPCs // Transfer classes for NPCs
if (!keepClass && d.data.details.cr) { if (!keepClass && d.data.details.cr) {
d.items.push({ d.items.push({
type: 'class', type: "class",
name: game.i18n.localize('SW5E.PolymorphTmpClass'), name: game.i18n.localize("SW5E.PolymorphTmpClass"),
data: {levels: d.data.details.cr} data: {levels: d.data.details.cr}
}); });
} }
@ -1667,16 +1723,27 @@ export default class Actor5e extends Actor {
// Update regular Actors by creating a new Actor with the Polymorphed data // Update regular Actors by creating a new Actor with the Polymorphed data
await this.sheet.close(); await this.sheet.close();
Hooks.callAll('sw5e.transformActor', this, target, d, { Hooks.callAll("sw5e.transformActor", this, target, d, {
keepPhysical, keepMental, keepSaves, keepSkills, mergeSaves, mergeSkills, keepPhysical,
keepClass, keepFeats, keepPowers, keepItems, keepBio, keepVision, transformTokens keepMental,
keepSaves,
keepSkills,
mergeSaves,
mergeSkills,
keepClass,
keepFeats,
keepPowers,
keepItems,
keepBio,
keepVision,
transformTokens
}); });
const newActor = await this.constructor.create(d, {renderSheet: true}); const newActor = await this.constructor.create(d, {renderSheet: true});
// Update placed Token instances // Update placed Token instances
if (!transformTokens) return; if (!transformTokens) return;
const tokens = this.getActiveTokens(true); const tokens = this.getActiveTokens(true);
const updates = tokens.map(t => { const updates = tokens.map((t) => {
const newTokenData = foundry.utils.deepClone(d.token); const newTokenData = foundry.utils.deepClone(d.token);
if (!t.data.actorLink) newTokenData.actorData = newActor.data; if (!t.data.actorLink) newTokenData.actorData = newActor.data;
newTokenData._id = t.data._id; newTokenData._id = t.data._id;
@ -1711,14 +1778,14 @@ export default class Actor5e extends Actor {
} }
// Obtain a reference to the original actor // Obtain a reference to the original actor
const original = game.actors.get(this.getFlag('sw5e', 'originalActor')); const original = game.actors.get(this.getFlag("sw5e", "originalActor"));
if (!original) return; if (!original) return;
// Get the Tokens which represent this actor // Get the Tokens which represent this actor
if (canvas.ready) { if (canvas.ready) {
const tokens = this.getActiveTokens(true); const tokens = this.getActiveTokens(true);
const tokenData = await original.getTokenData(); const tokenData = await original.getTokenData();
const tokenUpdates = tokens.map(t => { const tokenUpdates = tokens.map((t) => {
const update = duplicate(tokenData); const update = duplicate(tokenData);
update._id = t.id; update._id = t.id;
delete update.x; delete update.x;
@ -1745,16 +1812,16 @@ export default class Actor5e extends Actor {
*/ */
static addDirectoryContextOptions(html, entryOptions) { static addDirectoryContextOptions(html, entryOptions) {
entryOptions.push({ entryOptions.push({
name: 'SW5E.PolymorphRestoreTransformation', name: "SW5E.PolymorphRestoreTransformation",
icon: '<i class="fas fa-backward"></i>', icon: '<i class="fas fa-backward"></i>',
callback: li => { callback: (li) => {
const actor = game.actors.get(li.data('entityId')); const actor = game.actors.get(li.data("entityId"));
return actor.revertOriginalForm(); return actor.revertOriginalForm();
}, },
condition: li => { condition: (li) => {
const allowed = game.settings.get("sw5e", "allowPolymorphing"); const allowed = game.settings.get("sw5e", "allowPolymorphing");
if (!allowed && !game.user.isGM) return false; if (!allowed && !game.user.isGM) return false;
const actor = game.actors.get(li.data('entityId')); const actor = game.actors.get(li.data("entityId"));
return actor && actor.isPolymorphed; return actor && actor.isPolymorphed;
} }
}); });
@ -1778,7 +1845,7 @@ export default class Actor5e extends Actor {
} }
let type = localizedType; let type = localizedType;
if (!!typeData.swarm) { if (!!typeData.swarm) {
type = game.i18n.format('SW5E.CreatureSwarmPhrase', { type = game.i18n.format("SW5E.CreatureSwarmPhrase", {
size: game.i18n.localize(CONFIG.SW5E.actorSizes[typeData.swarm]), size: game.i18n.localize(CONFIG.SW5E.actorSizes[typeData.swarm]),
type: localizedType type: localizedType
}); });
@ -1795,7 +1862,9 @@ export default class Actor5e extends Actor {
* @deprecated since sw5e 0.97 * @deprecated since sw5e 0.97
*/ */
getPowerDC(ability) { getPowerDC(ability) {
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`); console.warn(
`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`
);
return this.data.data.abilities[ability]?.dc; return this.data.data.abilities[ability]?.dc;
} }

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js"; import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from '../../../config.js'; import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/** /**
@ -58,7 +58,8 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
get template() { get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html"; if (!game.user.isGM && this.actor.limited)
return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`; return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
} }
@ -66,7 +67,6 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
getData(options) { getData(options) {
// Basic data // Basic data
let isOwner = this.actor.isOwner; let isOwner = this.actor.isOwner;
const data = { const data = {
@ -78,7 +78,7 @@ export default class ActorSheet5e extends ActorSheet {
isCharacter: this.actor.type === "character", isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc", isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship", isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === 'vehicle', isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E, config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor) rollData: this.actor.getRollData.bind(this.actor)
}; };
@ -137,7 +137,7 @@ export default class ActorSheet5e extends ActorSheet {
data.effects = prepareActiveEffectCategories(this.actor.effects); data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet // Return data to the sheet
return data return data;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -156,31 +156,35 @@ export default class ActorSheet5e extends ActorSheet {
let speeds = [ let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], [
movement.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
] ];
if (largestPrimary) { if (largestPrimary) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
} }
// Filter and sort speeds on their values // Filter and sort speeds on their values
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary // Case 1: Largest as primary
if (largestPrimary) { if (largestPrimary) {
let primary = speeds.shift(); let primary = speeds.shift();
return { return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`, primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map(s => s[1]).join(", ") special: speeds.map((s) => s[1]).join(", ")
} };
} }
// Case 2: Walk as primary // Case 2: Walk as primary
else { else {
return { return {
primary: `${movement.walk || 0} ${movement.units}`, primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
} };
} }
} }
@ -190,7 +194,7 @@ export default class ActorSheet5e extends ActorSheet {
const senses = actorData.data.attributes.senses || {}; const senses = actorData.data.attributes.senses || {};
const tags = {}; const tags = {};
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) { for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[k] ?? 0 const v = senses[k] ?? 0;
if (v === 0) continue; if (v === 0) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
} }
@ -207,14 +211,14 @@ export default class ActorSheet5e extends ActorSheet {
*/ */
_prepareTraits(traits) { _prepareTraits(traits) {
const map = { const map = {
"dr": CONFIG.SW5E.damageResistanceTypes, dr: CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes, di: CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes, dv: CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes, ci: CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages, languages: CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies, armorProf: CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies, weaponProf: CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies toolProf: CONFIG.SW5E.toolProficiencies
}; };
for (let [t, choices] of Object.entries(map)) { for (let [t, choices] of Object.entries(map)) {
const trait = traits[t]; const trait = traits[t];
@ -230,7 +234,7 @@ export default class ActorSheet5e extends ActorSheet {
// Add custom entry // Add custom entry
if (trait.custom) { if (trait.custom) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim()); trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
} }
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
} }
@ -252,8 +256,8 @@ export default class ActorSheet5e extends ActorSheet {
// Define some mappings // Define some mappings
const sections = { const sections = {
"atwill": -20, atwill: -20,
"innate": -10, innate: -10
}; };
// Label power slot uses headers // Label power slot uses headers
@ -270,12 +274,17 @@ export default class ActorSheet5e extends ActorSheet {
label: label, label: label,
usesSlots: i > 0, usesSlots: i > 0,
canCreate: owner, canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1), canPrepare: data.actor.type === "character" && i >= 1,
powers: [], powers: [],
uses: useLabels[i] || value || 0, uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0, slots: useLabels[i] || max || 0,
override: override || 0, override: override || 0,
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school}, dataset: {
"type": "power",
"level": prepMode in sections ? 1 : i,
"preparation.mode": prepMode,
"school": school
},
prop: sl prop: sl
}; };
}; };
@ -284,7 +293,7 @@ export default class ActorSheet5e extends ActorSheet {
const maxLevel = Array.fromRange(10).reduce((max, i) => { const maxLevel = Array.fromRange(10).reduce((max, i) => {
if (i === 0) return max; if (i === 0) return max;
const level = levels[`power${i}`]; const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i; if ((level.max || level.override) && i > max) max = i;
return max; return max;
}, 0); }, 0);
@ -298,7 +307,7 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Iterate over every power item, adding powers to the powerbook by section // Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => { powers.forEach((power) => {
const mode = power.data.preparation.mode || "prepared"; const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0; let s = power.data.level || 0;
const sl = `power${s}`; const sl = `power${s}`;
@ -341,13 +350,13 @@ export default class ActorSheet5e extends ActorSheet {
* @private * @private
*/ */
_filterItems(items, filters) { _filterItems(items, filters) {
return items.filter(item => { return items.filter((item) => {
const data = item.data; const data = item.data;
// Action usage // Action usage
for (let f of ["action", "bonus", "reaction"]) { for (let f of ["action", "bonus", "reaction"]) {
if (filters.has(f)) { if (filters.has(f)) {
if ((data.activation && (data.activation.type !== f))) return false; if (data.activation && data.activation.type !== f) return false;
} }
} }
@ -395,64 +404,62 @@ export default class ActorSheet5e extends ActorSheet {
/** @inheritdoc */ /** @inheritdoc */
activateListeners(html) { activateListeners(html) {
// Activate Item Filters // Activate Item Filters
const filterLists = html.find(".filter-list"); const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this)); filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries // Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
// View Item Sheets // View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this)); html.find(".item-edit").click(this._onItemEdit.bind(this));
// Editable Only Listeners // Editable Only Listeners
if (this.isEditable) { if (this.isEditable) {
// Input focus and update // Input focus and update
const inputs = html.find("input"); const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select()); inputs.focus((ev) => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency // Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency // Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this)); html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector // Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this)); html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags // Configure Special Flags
html.find('.config-button').click(this._onConfigMenu.bind(this)); html.find(".config-button").click(this._onConfigMenu.bind(this));
// Owned Item management // Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this)); html.find(".item-create").click(this._onItemCreate.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this)); html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find('.item-collapse').click(this._onItemCollapse.bind(this)); html.find(".item-collapse").click(this._onItemCollapse.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this)); html.find(".item-uses input")
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this)); .click((ev) => ev.target.select())
html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this)); .change(this._onUsesChange.bind(this));
html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this)); html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this));
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
// Active Effect management // Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
} }
// Owner Only Listeners // Owner Only Listeners
if (this.actor.isOwner) { if (this.actor.isOwner) {
// Ability Checks // Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this)); html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks // Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
// Item Rolling // Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event)); html.find(".item .item-image").click((event) => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
} }
// Otherwise remove rollable classes // Otherwise remove rollable classes
@ -547,9 +554,9 @@ export default class ActorSheet5e extends ActorSheet {
// Toggle next level - forward on click, backwards on right // Toggle next level - forward on click, backwards on right
if (event.type === "click") { if (event.type === "click") {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]); field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
} else if (event.type === "contextmenu") { } else if (event.type === "contextmenu") {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]); field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
} }
// Update the field value and save the form // Update the field value and save the form
@ -560,13 +567,13 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
async _onDropActor(event, data) { async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing')); const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
if (!canPolymorph) return false; if (!canPolymorph) return false;
// Get the target actor // Get the target actor
let sourceActor = null; let sourceActor = null;
if (data.pack) { if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack); const pack = game.packs.find((p) => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id); sourceActor = await pack.getEntity(data.id);
} else { } else {
sourceActor = game.actors.get(data.id); sourceActor = game.actors.get(data.id);
@ -574,35 +581,37 @@ export default class ActorSheet5e extends ActorSheet {
if (!sourceActor) return; if (!sourceActor) return;
// Define a function to record polymorph settings for future use // Define a function to record polymorph settings for future use
const rememberOptions = html => { const rememberOptions = (html) => {
const options = {}; const options = {};
html.find('input').each((i, el) => { html.find("input").each((i, el) => {
options[el.name] = el.checked; options[el.name] = el.checked;
}); });
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options); const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings); game.settings.set("sw5e", "polymorphSettings", settings);
return settings; return settings;
}; };
// Create and render the Dialog // Create and render the Dialog
return new Dialog({ return new Dialog(
title: game.i18n.localize('SW5E.PolymorphPromptTitle'), {
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: { content: {
options: game.settings.get('sw5e', 'polymorphSettings'), options: game.settings.get("sw5e", "polymorphSettings"),
i18n: SW5E.polymorphSettings, i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken isToken: this.actor.isToken
}, },
default: 'accept', default: "accept",
buttons: { buttons: {
accept: { accept: {
icon: '<i class="fas fa-check"></i>', icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'), label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html)) callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
}, },
wildshape: { wildshape: {
icon: '<i class="fas fa-paw"></i>', icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'), label: game.i18n.localize("SW5E.PolymorphWildShape"),
callback: html => this.actor.transformInto(sourceActor, { callback: (html) =>
this.actor.transformInto(sourceActor, {
keepBio: true, keepBio: true,
keepClass: true, keepClass: true,
keepMental: true, keepMental: true,
@ -613,45 +622,49 @@ export default class ActorSheet5e extends ActorSheet {
}, },
polymorph: { polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>', icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'), label: game.i18n.localize("SW5E.Polymorph"),
callback: html => this.actor.transformInto(sourceActor, { callback: (html) =>
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens transformTokens: rememberOptions(html).transformTokens
}) })
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel') label: game.i18n.localize("Cancel")
} }
} }
}, { },
classes: ['dialog', 'sw5e'], {
classes: ["dialog", "sw5e"],
width: 600, width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html' template: "systems/sw5e/templates/apps/polymorph-prompt.html"
}).render(true); }
).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor // Check to make sure items of this type are allowed on this actor
if (this.constructor.unsupportedItemTypes.has(itemData.type)) { if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", { return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
})); })
);
} }
// Create a Consumable power scroll on the Inventory tab // Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) { if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData); const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data; itemData = scroll.data;
} }
if (itemData.data) { if (itemData.data) {
// Ignore certain statuses // Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]); ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED // Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
@ -659,14 +672,14 @@ export default class ActorSheet5e extends ActorSheet {
// Stack identical consumables // Stack identical consumables
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find(i => { const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId"); const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) && return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
(i.type === "consumable");
}); });
if ( similarItem && itemData.name !== "Power Cell" ) { // Always create a new powercell instead of increasing quantity if (similarItem && itemData.name !== "Power Cell") {
// Always create a new powercell instead of increasing quantity
return similarItem.update({ return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
}); });
} }
} }
@ -712,7 +725,7 @@ export default class ActorSheet5e extends ActorSheet {
const item = this.actor.items.get(itemId); const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses; event.target.value = uses;
return item.update({ 'data.uses.value': uses }); return item.update({"data.uses.value": uses});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -740,7 +753,7 @@ export default class ActorSheet5e extends ActorSheet {
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.items.get(itemId);
return item.rollRecharge(); return item.rollRecharge();
}; }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -761,7 +774,7 @@ export default class ActorSheet5e extends ActorSheet {
} else { } else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`); let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`); let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`)); chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`));
div.append(props); div.append(props);
li.append(div.hide()); li.append(div.hide());
div.slideDown(200); div.slideDown(200);
@ -847,7 +860,7 @@ _onItemCollapse(event) {
_onIncrementClassLevel(event) { _onIncrementClassLevel(event) {
event.preventDefault(); event.preventDefault();
const div = event.currentTarget.closest(".character") const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li"); const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1]; const actorId = div.id.split("-")[1];
@ -857,7 +870,7 @@ _onItemCollapse(event) {
const item = actor.items.get(itemId); const item = actor.items.get(itemId);
let levels = item.data.data.levels; let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: (levels + 1) }}; const update = {_id: item.data._id, data: {levels: levels + 1}};
actor.updateEmbeddedDocuments("Item", [update]); actor.updateEmbeddedDocuments("Item", [update]);
} }
@ -871,7 +884,7 @@ _onItemCollapse(event) {
_onDecrementClassLevel(event) { _onDecrementClassLevel(event) {
event.preventDefault(); event.preventDefault();
const div = event.currentTarget.closest(".character") const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li"); const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1]; const actorId = div.id.split("-")[1];
@ -881,7 +894,7 @@ _onItemCollapse(event) {
const item = actor.items.get(itemId); const item = actor.items.get(itemId);
let levels = item.data.data.levels; let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: (levels - 1) }}; const update = {_id: item.data._id, data: {levels: levels - 1}};
actor.updateEmbeddedDocuments("Item", [update]); actor.updateEmbeddedDocuments("Item", [update]);
} }
@ -955,7 +968,7 @@ _onItemCollapse(event) {
const label = a.parentElement.querySelector("label"); const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options]; const choices = CONFIG.SW5E[a.dataset.options];
const options = {name: a.dataset.target, title: label.innerText, choices}; const options = {name: a.dataset.target, title: label.innerText, choices};
return new TraitSelector(this.actor, options).render(true) return new TraitSelector(this.actor, options).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -965,7 +978,7 @@ _onItemCollapse(event) {
let buttons = super._getHeaderButtons(); let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) { if (this.actor.isPolymorphed) {
buttons.unshift({ buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation', label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation", class: "restore-transformation",
icon: "fas fa-backward", icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm() onclick: () => this.actor.revertOriginalForm()

View file

@ -7,7 +7,6 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacterNew extends ActorSheet5e { export default class ActorSheet5eCharacterNew extends ActorSheet5e {
get template() { get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return "systems/sw5e/templates/actors/newActor/character-sheet.html"; return "systems/sw5e/templates/actors/newActor/character-sheet.html";
@ -17,17 +16,18 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @return {Object} * @return {Object}
*/ */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["swalt", "sw5e", "sheet", "actor", "character"], classes: ["swalt", "sw5e", "sheet", "actor", "character"],
blockFavTab: true, blockFavTab: true,
subTabs: null, subTabs: null,
width: 800, width: 800,
tabs: [{ tabs: [
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
}], }
]
}); });
} }
@ -56,10 +56,12 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => { sheetData["multiclassLabels"] = this.actor.itemTypes.class
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ') .map((c) => {
}).join(', '); return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
})
.join(", ");
// Return data for rendering // Return data for rendering
return sheetData; return sheetData;
@ -72,7 +74,6 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes // Categorize items as inventory, powerbook, features, and classes
const inventory = { const inventory = {
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}}, weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
@ -84,11 +85,27 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}; };
// Partition items by category // Partition items by category
let [items, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { let [
items,
forcepowers,
techpowers,
feats,
classes,
deployments,
deploymentfeatures,
ventures,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details // Item details
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.attunement = { item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: { [CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun", icon: "fa-sun",
@ -103,16 +120,18 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}[item.data.attunement]; }[item.data.attunement];
// Item usage // Item usage
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); item.isOnCooldown =
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
// Item toggle state // Item toggle state
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Primary Class // Primary Class
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass ); if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types // Classify items into types
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item); if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item);
@ -131,7 +150,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
else if (item.type === "lightsaberform") arr[14].push(item); else if (item.type === "lightsaberform") arr[14].push(item);
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item); else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
return arr; return arr;
}, [[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]); },
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); items = this._filterItems(items, this._filters.inventory);
@ -153,18 +174,89 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Organize Features // Organize Features
const features = { const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, classes: {
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, label: "SW5E.ItemTypeClassPl",
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, items: [],
deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true }, hasActions: false,
deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true }, dataset: {type: "class"},
ventures: { label: "SW5E.ItemTypeVenturePl", items: [], hasActions: false, dataset: {type: "venture"}, isVenture: true }, isClass: true
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, },
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, classfeatures: {
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, label: "SW5E.ItemTypeClassFeats",
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, items: [],
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, hasActions: true,
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, dataset: {type: "classfeature"},
isClassfeature: true
},
archetype: {
label: "SW5E.ItemTypeArchetype",
items: [],
hasActions: false,
dataset: {type: "archetype"},
isArchetype: true
},
deployments: {
label: "SW5E.ItemTypeDeploymentPl",
items: [],
hasActions: false,
dataset: {type: "deployment"},
isDeployment: true
},
deploymentfeatures: {
label: "SW5E.ItemTypeDeploymentFeaturePl",
items: [],
hasActions: true,
dataset: {type: "deploymentfeature"},
isDeploymentfeature: true
},
ventures: {
label: "SW5E.ItemTypeVenturePl",
items: [],
hasActions: false,
dataset: {type: "venture"},
isVenture: true
},
species: {
label: "SW5E.ItemTypeSpecies",
items: [],
hasActions: false,
dataset: {type: "species"},
isSpecies: true
},
background: {
label: "SW5E.ItemTypeBackground",
items: [],
hasActions: false,
dataset: {type: "background"},
isBackground: true
},
fightingstyles: {
label: "SW5E.ItemTypeFightingStylePl",
items: [],
hasActions: false,
dataset: {type: "fightingstyle"},
isFightingstyle: true
},
fightingmasteries: {
label: "SW5E.ItemTypeFightingMasteryPl",
items: [],
hasActions: false,
dataset: {type: "fightingmastery"},
isFightingmastery: true
},
lightsaberforms: {
label: "SW5E.ItemTypeLightsaberFormPl",
items: [],
hasActions: false,
dataset: {type: "lightsaberform"},
isLightsaberform: true
},
active: {
label: "SW5E.FeatureActive",
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}} passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
}; };
for (let f of feats) { for (let f of feats) {
@ -207,8 +299,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} } else {
else {
const isActive = getProperty(item.data, "equipped"); const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : ""; item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -231,19 +322,21 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); // html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
// Item State Toggling // Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this)); html.find(".item-toggle").click(this._onToggleItem.bind(this));
// Short and Long Rest // Short and Long Rest
html.find('.short-rest').click(this._onShortRest.bind(this)); html.find(".short-rest").click(this._onShortRest.bind(this));
html.find('.long-rest').click(this._onLongRest.bind(this)); html.find(".long-rest").click(this._onLongRest.bind(this));
// Rollable sheet actions // Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
// Send Languages to Chat onClick // Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click(event => { html.find('[data-options="share-languages"]').click((event) => {
event.preventDefault(); event.preventDefault();
let langs = this.actor.data.data.traits.languages.value.map(l => CONFIG.SW5E.languages[l] || l).join(", "); let langs = this.actor.data.data.traits.languages.value
.map((l) => CONFIG.SW5E.languages[l] || l)
.join(", ");
let custom = this.actor.data.data.traits.languages.custom; let custom = this.actor.data.data.traits.languages.custom;
if (custom) langs += ", " + custom.replace(/;/g, ","); if (custom) langs += ", " + custom.replace(/;/g, ",");
let content = ` let content = `
@ -279,9 +372,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}); });
// Item Delete Confirmation // Item Delete Confirmation
html.find('.item-delete').off("click"); html.find(".item-delete").off("click");
html.find('.item-delete').click(event => { html.find(".item-delete").click((event) => {
let li = $(event.currentTarget).parents('.item'); let li = $(event.currentTarget).parents(".item");
let itemId = li.attr("data-item-id"); let itemId = li.attr("data-item-id");
let item = this.actor.items.get(itemId); let item = this.actor.items.get(itemId);
new Dialog({ new Dialog({
@ -290,17 +383,17 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
buttons: { buttons: {
Yes: { Yes: {
icon: '<i class="fa fa-check"></i>', icon: '<i class="fa fa-check"></i>',
label: 'Yes', label: "Yes",
callback: dlg => { callback: (dlg) => {
this.actor.deleteOwnedItem(itemId); this.actor.deleteOwnedItem(itemId);
} }
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: 'No' label: "No"
}
}, },
}, default: "cancel"
default: 'cancel'
}).render(true); }).render(true);
}); });
} }
@ -325,7 +418,6 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle toggling the state of an Owned Item within the Actor * Handle toggling the state of an Owned Item within the Actor
* @param {Event} event The triggering click event * @param {Event} event The triggering click event
@ -369,10 +461,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Increment the number of class levels of a character instead of creating a new item // Increment the number of class levels of a character instead of creating a new item
if (itemData.type === "class") { if (itemData.type === "class") {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0; let priorLevel = cls?.data.data.levels ?? 0;
if (!!cls) { if (!!cls) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
@ -457,9 +548,9 @@ async function addFavorites(app, html, data) {
value: data.actor.data.powers.power9.value, value: data.actor.data.powers.power9.value,
max: data.actor.data.powers.power9.max max: data.actor.data.powers.power9.max
} }
} };
let powerCount = 0 let powerCount = 0;
let items = data.actor.items; let items = data.actor.items;
for (let item of items) { for (let item of items) {
if (item.type == "class") continue; if (item.type == "class") continue;
@ -470,24 +561,28 @@ async function addFavorites(app, html, data) {
} }
let isFav = item.flags.favtab.isFavourite; let isFav = item.flags.favtab.isFavourite;
if (app.options.editable) { if (app.options.editable) {
let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`); let favBtn = $(
favBtn.click(ev => { `<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${
isFav ? "Remove from Favourites" : "Add to Favourites"
}"><i class="fas fa-star"></i></a>`
);
favBtn.click((ev) => {
app.actor.items.get(item.data._id).update({ app.actor.items.get(item.data._id).update({
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite "flags.favtab.isFavourite": !item.flags.favtab.isFavourite
}); });
}); });
html.find(`.item[data-item-id="${item.data._id}"]`).find('.item-controls').prepend(favBtn); html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn);
} }
if (isFav) { if (isFav) {
item.powerComps = ""; item.powerComps = "";
if (item.data.components) { if (item.data.components) {
let comps = item.data.components; let comps = item.data.components;
let v = (comps.vocal) ? "V" : ""; let v = comps.vocal ? "V" : "";
let s = (comps.somatic) ? "S" : ""; let s = comps.somatic ? "S" : "";
let m = (comps.material) ? "M" : ""; let m = comps.material ? "M" : "";
let c = !!(comps.concentration); let c = !!comps.concentration;
let r = !!(comps.ritual); let r = !!comps.ritual;
item.powerComps = `${v}${s}${m}`; item.powerComps = `${v}${s}${m}`;
item.powerCon = c; item.powerCon = c;
item.powerRit = r; item.powerRit = r;
@ -495,15 +590,15 @@ async function addFavorites(app, html, data) {
item.editable = app.options.editable; item.editable = app.options.editable;
switch (item.type) { switch (item.type) {
case 'feat': case "feat":
if (item.flags.favtab.sort === undefined) { if (item.flags.favtab.sort === undefined) {
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
} }
favFeats.push(item); favFeats.push(item);
break; break;
case 'power': case "power":
if (item.data.preparation.mode) { if (item.data.preparation.mode) {
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})` item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`;
} }
if (item.data.level) { if (item.data.level) {
favPowers[item.data.level].powers.push(item); favPowers[item.data.level].powers.push(item);
@ -529,60 +624,60 @@ async function addFavorites(app, html, data) {
// html.find('.favourite .item-controls').css('flex', '0 0 22px'); // html.find('.favourite .item-controls').css('flex', '0 0 22px');
// } // }
let tabContainer = html.find('.favtabtarget'); let tabContainer = html.find(".favtabtarget");
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
data.favPowers = powerCount > 0 ? favPowers : false; data.favPowers = powerCount > 0 ? favPowers : false;
data.editable = app.options.editable; data.editable = app.options.editable;
await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']); await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]);
let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data)); let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data));
favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event)); favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event));
if (app.options.editable) { if (app.options.editable) {
favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev)); favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev));
let handler = ev => app._onDragStart(ev); let handler = (ev) => app._onDragStart(ev);
favtabHtml.find('.item').each((i, li) => { favtabHtml.find(".item").each((i, li) => {
if (li.classList.contains("inventory-header")) return; if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true); li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false); li.addEventListener("dragstart", handler, false);
}); });
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
favtabHtml.find('.item-edit').click(ev => { favtabHtml.find(".item-edit").click((ev) => {
let itemId = $(ev.target).parents('.item')[0].dataset.itemId; let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
app.actor.items.get(itemId).sheet.render(true); app.actor.items.get(itemId).sheet.render(true);
}); });
favtabHtml.find('.item-fav').click(ev => { favtabHtml.find(".item-fav").click((ev) => {
let itemId = $(ev.target).parents('.item')[0].dataset.itemId; let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite;
app.actor.items.get(itemId).update({ app.actor.items.get(itemId).update({
"flags.favtab.isFavourite": val "flags.favtab.isFavourite": val
}); });
}); });
// Sorting // Sorting
favtabHtml.find('.item').on('drop', ev => { favtabHtml.find(".item").on("drop", (ev) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain')); let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain"));
// if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return;
if (dropData.actorId !== app.actor.id) return; if (dropData.actorId !== app.actor.id) return;
let list = null; let list = null;
if (dropData.data.type === 'feat') list = favFeats; if (dropData.data.type === "feat") list = favFeats;
else list = favItems; else list = favItems;
let dragSource = list.find(i => i.data._id === dropData.data._id); let dragSource = list.find((i) => i.data._id === dropData.data._id);
let siblings = list.filter(i => i.data._id !== dropData.data._id); let siblings = list.filter((i) => i.data._id !== dropData.data._id);
let targetId = ev.target.closest('.item').dataset.itemId; let targetId = ev.target.closest(".item").dataset.itemId;
let dragTarget = siblings.find(s => s.data._id === targetId); let dragTarget = siblings.find((s) => s.data._id === targetId);
if (dragTarget === undefined) return; if (dragTarget === undefined) return;
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
target: dragTarget, target: dragTarget,
siblings: siblings, siblings: siblings,
sortKey: 'flags.favtab.sort' sortKey: "flags.favtab.sort"
}); });
const updateData = sortUpdates.map(u => { const updateData = sortUpdates.map((u) => {
const update = u.update; const update = u.update;
update._id = u.target.data._id; update._id = u.target.data._id;
return update; return update;
@ -611,46 +706,44 @@ async function addSubTabs(app, html, data) {
if (data.options.subTabs == null) { if (data.options.subTabs == null) {
//let subTabs = []; //{subgroup: '', target: '', active: false} //let subTabs = []; //{subgroup: '', target: '', active: false}
data.options.subTabs = {}; data.options.subTabs = {};
html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => { html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => {
let subgroup = el.getAttribute('data-subgroup'); let subgroup = el.getAttribute("data-subgroup");
let target = el.getAttribute('data-target'); let target = el.getAttribute("data-target");
let targetObj = {target: target, active: el.classList.contains("active")} let targetObj = {target: target, active: el.classList.contains("active")};
if (data.options.subTabs.hasOwnProperty(subgroup)) { if (data.options.subTabs.hasOwnProperty(subgroup)) {
data.options.subTabs[subgroup].push(targetObj); data.options.subTabs[subgroup].push(targetObj);
} else { } else {
data.options.subTabs[subgroup] = []; data.options.subTabs[subgroup] = [];
data.options.subTabs[subgroup].push(targetObj); data.options.subTabs[subgroup].push(targetObj);
} }
}) });
} }
for (const group in data.options.subTabs) { for (const group in data.options.subTabs) {
data.options.subTabs[group].forEach(tab => { data.options.subTabs[group].forEach((tab) => {
if (tab.active) { if (tab.active) {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active'); html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active");
} else { } else {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active'); html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active");
} }
}) });
} }
html.find('[data-subgroup-selection]').children().on('click', event => { html.find("[data-subgroup-selection]")
let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup'); .children()
let target = event.target.closest('[data-target]').getAttribute('data-target'); .on("click", (event) => {
html.find(`[data-subgroup=${subgroup}]`).removeClass('active'); let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup");
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active'); let target = event.target.closest("[data-target]").getAttribute("data-target");
let tabId = data.options.subTabs[subgroup].find(tab => { html.find(`[data-subgroup=${subgroup}]`).removeClass("active");
return tab.target == target html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active");
let tabId = data.options.subTabs[subgroup].find((tab) => {
return tab.target == target;
}); });
data.options.subTabs[subgroup].map(el => { data.options.subTabs[subgroup].map((el) => {
el.active = el.target == target; el.active = el.target == target;
return el; return el;
}) });
});
})
} }
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {

View file

@ -6,7 +6,6 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPCNew extends ActorSheet5e { export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */ /** @override */
get template() { get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
@ -17,11 +16,13 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"], classes: ["sw5e", "sheet", "actor", "npc"],
width: 800, width: 800,
tabs: [{ tabs: [
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
}], }
]
}); });
} }
@ -37,28 +38,41 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, weapons: {
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, label: game.i18n.localize("SW5E.AttackPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => { let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); item.isOnCooldown =
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item); if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item); else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item); else arr[2].push(item);
return arr; return arr;
}, [[], [], []]); },
[[], [], []]
);
// Apply item filters // Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -75,8 +89,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
else if (item.type === "feat") { else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item); if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} } else features.equipment.items.push(item);
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -107,7 +120,6 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */ /** @override */
async _updateObject(event, formData) { async _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -145,4 +157,3 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
} }
} }

View file

@ -6,7 +6,6 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eStarship extends ActorSheet5e { export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */ /** @override */
get template() { get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
@ -17,11 +16,13 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"], classes: ["sw5e", "sheet", "actor", "starship"],
width: 800, width: 800,
tabs: [{ tabs: [
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
}], }
]
}); });
} }
@ -32,29 +33,47 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} }, starshipfeatures: {
starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} } label: game.i18n.localize("SW5E.StarshipfeaturePl"),
items: [],
hasActions: true,
dataset: {type: "starshipfeature"}
},
starshipmods: {
label: game.i18n.localize("SW5E.StarshipmodPl"),
items: [],
hasActions: false,
dataset: {type: "starshipmod"}
}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => { let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); item.isOnCooldown =
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item); if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item); else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item); else arr[2].push(item);
return arr; return arr;
}, [[], [], []]); },
[[], [], []]
);
// Apply item filters // Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -71,14 +90,11 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
else if (item.type === "feat") { else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item); if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} } else if (item.type === "starshipfeature") {
else if ( item.type === "starshipfeature" ) {
features.starshipfeatures.items.push(item); features.starshipfeatures.items.push(item);
} } else if (item.type === "starshipmod") {
else if ( item.type === "starshipmod" ) {
features.starshipmods.items.push(item); features.starshipmods.items.push(item);
} } else features.equipment.items.push(item);
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -87,7 +103,6 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
// data.techPowerbook = techPowerbook; // data.techPowerbook = techPowerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
@ -115,7 +130,6 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */ /** @override */
async _updateObject(event, formData) { async _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";

View file

@ -25,13 +25,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Creates a new cargo entry for a vehicle Actor. * Creates a new cargo entry for a vehicle Actor.
*/ */
static get newCargo() { static get newCargo() {
return { return {
name: '', name: "",
quantity: 1 quantity: 1
}; };
} }
@ -46,7 +45,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_computeEncumbrance(totalWeight, actorData) { _computeEncumbrance(totalWeight, actorData) {
// Compute currency weight // Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -75,25 +73,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareCrewedItem(item) { _prepareCrewedItem(item) {
// Determine crewed status // Determine crewed status
const isCrewed = item.data.crewed; const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : ''; item.toggleClass = isCrewed ? "active" : "";
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
// Handle crew actions // Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') { if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost; item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === .5) item.cover = '½'; if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === .75) item.cover = '¾'; else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = '—'; else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = '—'; if (item.crew < 1 || item.crew === null) item.crew = "—";
} }
// Prepare vehicle weapons // Prepare vehicle weapons
if (item.type === 'equipment' || item.type === 'weapon') { if (item.type === "equipment" || item.type === "weapon") {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
} }
} }
@ -104,111 +101,125 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
const cargoColumns = [{ const cargoColumns = [
label: game.i18n.localize('SW5E.Quantity'), {
css: 'item-qty', label: game.i18n.localize("SW5E.Quantity"),
property: 'quantity', css: "item-qty",
editable: 'Number' property: "quantity",
}]; editable: "Number"
}
];
const equipmentColumns = [{ const equipmentColumns = [
label: game.i18n.localize('SW5E.Quantity'), {
css: 'item-qty', label: game.i18n.localize("SW5E.Quantity"),
property: 'data.quantity' css: "item-qty",
}, { property: "data.quantity"
label: game.i18n.localize('SW5E.AC'), },
css: 'item-ac', {
property: 'data.armor.value' label: game.i18n.localize("SW5E.AC"),
}, { css: "item-ac",
label: game.i18n.localize('SW5E.HP'), property: "data.armor.value"
css: 'item-hp', },
property: 'data.hp.value', {
editable: 'Number' label: game.i18n.localize("SW5E.HP"),
}, { css: "item-hp",
label: game.i18n.localize('SW5E.Threshold'), property: "data.hp.value",
css: 'item-threshold', editable: "Number"
property: 'threshold' },
}]; {
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = { const features = {
actions: { actions: {
label: game.i18n.localize('SW5E.ActionPl'), label: game.i18n.localize("SW5E.ActionPl"),
items: [], items: [],
crewable: true, crewable: true,
dataset: {type: 'feat', 'activation.type': 'crew'}, dataset: {"type": "feat", "activation.type": "crew"},
columns: [{ columns: [
label: game.i18n.localize('SW5E.VehicleCrew'), {
css: 'item-crew', label: game.i18n.localize("SW5E.VehicleCrew"),
property: 'crew' css: "item-crew",
}, { property: "crew"
label: game.i18n.localize('SW5E.Cover'), },
css: 'item-cover', {
property: 'cover' label: game.i18n.localize("SW5E.Cover"),
}] css: "item-cover",
property: "cover"
}
]
}, },
equipment: { equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'), label: game.i18n.localize("SW5E.ItemTypeEquipment"),
items: [], items: [],
crewable: true, crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'}, dataset: {"type": "equipment", "armor.type": "vehicle"},
columns: equipmentColumns columns: equipmentColumns
}, },
passive: { passive: {
label: game.i18n.localize('SW5E.Features'), label: game.i18n.localize("SW5E.Features"),
items: [], items: [],
dataset: {type: 'feat'} dataset: {type: "feat"}
}, },
reactions: { reactions: {
label: game.i18n.localize('SW5E.ReactionPl'), label: game.i18n.localize("SW5E.ReactionPl"),
items: [], items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'} dataset: {"type": "feat", "activation.type": "reaction"}
}, },
weapons: { weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [], items: [],
crewable: true, crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'}, dataset: {"type": "weapon", "weapon-type": "siege"},
columns: equipmentColumns columns: equipmentColumns
} }
}; };
const cargo = { const cargo = {
crew: { crew: {
label: game.i18n.localize('SW5E.VehicleCrew'), label: game.i18n.localize("SW5E.VehicleCrew"),
items: data.data.cargo.crew, items: data.data.cargo.crew,
css: 'cargo-row crew', css: "cargo-row crew",
editableName: true, editableName: true,
dataset: {type: 'crew'}, dataset: {type: "crew"},
columns: cargoColumns columns: cargoColumns
}, },
passengers: { passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'), label: game.i18n.localize("SW5E.VehiclePassengers"),
items: data.data.cargo.passengers, items: data.data.cargo.passengers,
css: 'cargo-row passengers', css: "cargo-row passengers",
editableName: true, editableName: true,
dataset: {type: 'passengers'}, dataset: {type: "passengers"},
columns: cargoColumns columns: cargoColumns
}, },
cargo: { cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'), label: game.i18n.localize("SW5E.VehicleCargo"),
items: [], items: [],
dataset: {type: 'loot'}, dataset: {type: "loot"},
columns: [{ columns: [
label: game.i18n.localize('SW5E.Quantity'), {
css: 'item-qty', label: game.i18n.localize("SW5E.Quantity"),
property: 'data.quantity', css: "item-qty",
editable: 'Number' property: "data.quantity",
}, { editable: "Number"
label: game.i18n.localize('SW5E.Price'), },
css: 'item-price', {
property: 'data.price', label: game.i18n.localize("SW5E.Price"),
editable: 'Number' css: "item-price",
}, { property: "data.price",
label: game.i18n.localize('SW5E.Weight'), editable: "Number"
css: 'item-weight', },
property: 'data.weight', {
editable: 'Number' label: game.i18n.localize("SW5E.Weight"),
}] css: "item-weight",
property: "data.weight",
editable: "Number"
}
]
} }
}; };
@ -234,8 +245,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
features.equipment.items.push(item); features.equipment.items.push(item);
break; break;
case "feat": case "feat":
if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item); if (!item.data.activation.type || item.data.activation.type === "none")
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item); else features.actions.items.push(item);
break; break;
default: default:
@ -259,21 +271,21 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if (!this.isEditable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this)); html.find(".item-toggle").click(this._onToggleItem.bind(this));
html.find('.item-hp input') html.find(".item-hp input")
.click(evt => evt.target.select()) .click((evt) => evt.target.select())
.change(this._onHPChange.bind(this)); .change(this._onHPChange.bind(this));
html.find('.item:not(.cargo-row) input[data-property]') html.find(".item:not(.cargo-row) input[data-property]")
.click(evt => evt.target.select()) .click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find('.cargo-row input') html.find(".cargo-row input")
.click(evt => evt.target.select()) .click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this)); .change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) { if (this.actor.data.data.attributes.actions.stations) {
html.find('.counter.actions, .counter.action-thresholds').hide(); html.find(".counter.actions, .counter.action-thresholds").hide();
} }
} }
@ -288,9 +300,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) { _onCargoRowChange(event) {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const row = target.closest('.item'); const row = target.closest(".item");
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers'; const property = row.classList.contains("crew") ? "crew" : "passengers";
// Get the cargo entry // Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
@ -298,10 +310,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
if (!entry) return null; if (!entry) return null;
// Update the cargo value // Update the cargo value
const key = target.dataset.property || 'name'; const key = target.dataset.property || "name";
const type = target.dataset.dtype; const type = target.dataset.dtype;
let value = target.value; let value = target.value;
if (type === 'Number') value = Number(value); if (type === "Number") value = Number(value);
entry[key] = value; entry[key] = value;
// Perform the Actor update // Perform the Actor update
@ -318,14 +330,18 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onEditInSheet(event) { _onEditInSheet(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId; const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property; const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype; const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value; let value = event.currentTarget.value;
switch (type) { switch (type) {
case 'Number': value = parseInt(value); break; case "Number":
case 'Boolean': value = value === 'true'; break; value = parseInt(value);
break;
case "Boolean":
value = value === "true";
break;
} }
return item.update({[`${property}`]: value}); return item.update({[`${property}`]: value});
} }
@ -342,7 +358,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const type = target.dataset.type; const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') { if (type === "crew" || type === "passengers") {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo); cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
@ -360,10 +376,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const row = event.currentTarget.closest('.item'); const row = event.currentTarget.closest(".item");
if (row.classList.contains('cargo-row')) { if (row.classList.contains("cargo-row")) {
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers'; const type = row.classList.contains("crew") ? "crew" : "passengers";
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -376,7 +392,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData); return super._onDropItemCreate(itemData);
} }
@ -391,11 +407,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onHPChange(event) { _onHPChange(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId; const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp; event.currentTarget.value = hp;
return item.update({'data.hp.value': hp}); return item.update({"data.hp.value": hp});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -408,9 +424,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId; const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed; const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed}); return item.update({"data.crewed": !crewed});
}
} }
};

View file

@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js"; import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from '../../../config.js'; import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/** /**
@ -54,7 +54,6 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
get template() { get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html"; if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
@ -65,7 +64,6 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
getData(options) { getData(options) {
// Basic data // Basic data
let isOwner = this.actor.isOwner; let isOwner = this.actor.isOwner;
const data = { const data = {
@ -77,7 +75,7 @@ export default class ActorSheet5e extends ActorSheet {
isCharacter: this.actor.type === "character", isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc", isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship", isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === 'vehicle', isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E, config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor) rollData: this.actor.getRollData.bind(this.actor)
}; };
@ -132,7 +130,7 @@ export default class ActorSheet5e extends ActorSheet {
data.effects = prepareActiveEffectCategories(this.actor.effects); data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet // Return data to the sheet
return data return data;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -151,31 +149,35 @@ export default class ActorSheet5e extends ActorSheet {
let speeds = [ let speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], [
movement.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
] ];
if (largestPrimary) { if (largestPrimary) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
} }
// Filter and sort speeds on their values // Filter and sort speeds on their values
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary // Case 1: Largest as primary
if (largestPrimary) { if (largestPrimary) {
let primary = speeds.shift(); let primary = speeds.shift();
return { return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`, primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map(s => s[1]).join(", ") special: speeds.map((s) => s[1]).join(", ")
} };
} }
// Case 2: Walk as primary // Case 2: Walk as primary
else { else {
return { return {
primary: `${movement.walk || 0} ${movement.units}`, primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
} };
} }
} }
@ -185,7 +187,7 @@ export default class ActorSheet5e extends ActorSheet {
const senses = actorData.data.attributes.senses || {}; const senses = actorData.data.attributes.senses || {};
const tags = {}; const tags = {};
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) { for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[k] ?? 0 const v = senses[k] ?? 0;
if (v === 0) continue; if (v === 0) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
} }
@ -202,14 +204,14 @@ export default class ActorSheet5e extends ActorSheet {
*/ */
_prepareTraits(traits) { _prepareTraits(traits) {
const map = { const map = {
"dr": CONFIG.SW5E.damageResistanceTypes, dr: CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes, di: CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes, dv: CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes, ci: CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages, languages: CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies, armorProf: CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies, weaponProf: CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies toolProf: CONFIG.SW5E.toolProficiencies
}; };
for (let [t, choices] of Object.entries(map)) { for (let [t, choices] of Object.entries(map)) {
const trait = traits[t]; const trait = traits[t];
@ -225,7 +227,7 @@ export default class ActorSheet5e extends ActorSheet {
// Add custom entry // Add custom entry
if (trait.custom) { if (trait.custom) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim()); trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
} }
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
} }
@ -246,9 +248,9 @@ export default class ActorSheet5e extends ActorSheet {
// Define some mappings // Define some mappings
const sections = { const sections = {
"atwill": -20, atwill: -20,
"innate": -10, innate: -10,
"pact": 0.5 pact: 0.5
}; };
// Label power slot uses headers // Label power slot uses headers
@ -265,7 +267,7 @@ export default class ActorSheet5e extends ActorSheet {
label: label, label: label,
usesSlots: i > 0, usesSlots: i > 0,
canCreate: owner, canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1), canPrepare: data.actor.type === "character" && i >= 1,
powers: [], powers: [],
uses: useLabels[i] || value || 0, uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0, slots: useLabels[i] || max || 0,
@ -279,7 +281,7 @@ export default class ActorSheet5e extends ActorSheet {
const maxLevel = Array.fromRange(10).reduce((max, i) => { const maxLevel = Array.fromRange(10).reduce((max, i) => {
if (i === 0) return max; if (i === 0) return max;
const level = levels[`power${i}`]; const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i; if ((level.max || level.override) && i > max) max = i;
return max; return max;
}, 0); }, 0);
@ -309,7 +311,7 @@ export default class ActorSheet5e extends ActorSheet {
} }
// Iterate over every power item, adding powers to the powerbook by section // Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => { powers.forEach((power) => {
const mode = power.data.preparation.mode || "prepared"; const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0; let s = power.data.level || 0;
const sl = `power${s}`; const sl = `power${s}`;
@ -352,13 +354,13 @@ export default class ActorSheet5e extends ActorSheet {
* @private * @private
*/ */
_filterItems(items, filters) { _filterItems(items, filters) {
return items.filter(item => { return items.filter((item) => {
const data = item.data; const data = item.data;
// Action usage // Action usage
for (let f of ["action", "bonus", "reaction"]) { for (let f of ["action", "bonus", "reaction"]) {
if (filters.has(f)) { if (filters.has(f)) {
if ((data.activation && (data.activation.type !== f))) return false; if (data.activation && data.activation.type !== f) return false;
} }
} }
@ -405,61 +407,59 @@ export default class ActorSheet5e extends ActorSheet {
/** @inheritdoc */ /** @inheritdoc */
activateListeners(html) { activateListeners(html) {
// Activate Item Filters // Activate Item Filters
const filterLists = html.find(".filter-list"); const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this)); filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries // Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
// View Item Sheets // View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this)); html.find(".item-edit").click(this._onItemEdit.bind(this));
// Editable Only Listeners // Editable Only Listeners
if (this.isEditable) { if (this.isEditable) {
// Input focus and update // Input focus and update
const inputs = html.find("input"); const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select()); inputs.focus((ev) => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency // Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency // Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this)); html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector // Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this)); html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags // Configure Special Flags
html.find('.config-button').click(this._onConfigMenu.bind(this)); html.find(".config-button").click(this._onConfigMenu.bind(this));
// Owned Item management // Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this)); html.find(".item-create").click(this._onItemCreate.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this)); html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this)); html.find(".item-uses input")
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this)); .click((ev) => ev.target.select())
.change(this._onUsesChange.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
// Active Effect management // Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
} }
// Owner Only Listeners // Owner Only Listeners
if (this.actor.isOwner) { if (this.actor.isOwner) {
// Ability Checks // Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this)); html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks // Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
// Item Rolling // Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event)); html.find(".item .item-image").click((event) => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
} }
// Otherwise remove rollable classes // Otherwise remove rollable classes
@ -554,9 +554,9 @@ export default class ActorSheet5e extends ActorSheet {
// Toggle next level - forward on click, backwards on right // Toggle next level - forward on click, backwards on right
if (event.type === "click") { if (event.type === "click") {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]); field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
} else if (event.type === "contextmenu") { } else if (event.type === "contextmenu") {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]); field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
} }
// Update the field value and save the form // Update the field value and save the form
@ -567,13 +567,13 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */ /** @override */
async _onDropActor(event, data) { async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing')); const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
if (!canPolymorph) return false; if (!canPolymorph) return false;
// Get the target actor // Get the target actor
let sourceActor = null; let sourceActor = null;
if (data.pack) { if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack); const pack = game.packs.find((p) => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id); sourceActor = await pack.getEntity(data.id);
} else { } else {
sourceActor = game.actors.get(data.id); sourceActor = game.actors.get(data.id);
@ -581,35 +581,37 @@ export default class ActorSheet5e extends ActorSheet {
if (!sourceActor) return; if (!sourceActor) return;
// Define a function to record polymorph settings for future use // Define a function to record polymorph settings for future use
const rememberOptions = html => { const rememberOptions = (html) => {
const options = {}; const options = {};
html.find('input').each((i, el) => { html.find("input").each((i, el) => {
options[el.name] = el.checked; options[el.name] = el.checked;
}); });
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options); const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings); game.settings.set("sw5e", "polymorphSettings", settings);
return settings; return settings;
}; };
// Create and render the Dialog // Create and render the Dialog
return new Dialog({ return new Dialog(
title: game.i18n.localize('SW5E.PolymorphPromptTitle'), {
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: { content: {
options: game.settings.get('sw5e', 'polymorphSettings'), options: game.settings.get("sw5e", "polymorphSettings"),
i18n: SW5E.polymorphSettings, i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken isToken: this.actor.isToken
}, },
default: 'accept', default: "accept",
buttons: { buttons: {
accept: { accept: {
icon: '<i class="fas fa-check"></i>', icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'), label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html)) callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
}, },
wildshape: { wildshape: {
icon: '<i class="fas fa-paw"></i>', icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'), label: game.i18n.localize("SW5E.PolymorphWildShape"),
callback: html => this.actor.transformInto(sourceActor, { callback: (html) =>
this.actor.transformInto(sourceActor, {
keepBio: true, keepBio: true,
keepClass: true, keepClass: true,
keepMental: true, keepMental: true,
@ -620,46 +622,50 @@ export default class ActorSheet5e extends ActorSheet {
}, },
polymorph: { polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>', icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'), label: game.i18n.localize("SW5E.Polymorph"),
callback: html => this.actor.transformInto(sourceActor, { callback: (html) =>
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens transformTokens: rememberOptions(html).transformTokens
}) })
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel') label: game.i18n.localize("Cancel")
} }
} }
}, { },
classes: ['dialog', 'sw5e'], {
classes: ["dialog", "sw5e"],
width: 600, width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html' template: "systems/sw5e/templates/apps/polymorph-prompt.html"
}).render(true); }
).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor // Check to make sure items of this type are allowed on this actor
if (this.constructor.unsupportedItemTypes.has(itemData.type)) { if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", { return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
})); })
);
} }
// Create a Consumable power scroll on the Inventory tab // Create a Consumable power scroll on the Inventory tab
// TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) { if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData); const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data; itemData = scroll.data;
} }
if (itemData.data) { if (itemData.data) {
// Ignore certain statuses // Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]); ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED // Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
@ -667,14 +673,13 @@ export default class ActorSheet5e extends ActorSheet {
// Stack identical consumables // Stack identical consumables
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find(i => { const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId"); const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) && return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
(i.type === "consumable");
}); });
if (similarItem) { if (similarItem) {
return similarItem.update({ return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
}); });
} }
} }
@ -720,7 +725,7 @@ export default class ActorSheet5e extends ActorSheet {
const item = this.actor.items.get(itemId); const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses; event.target.value = uses;
return item.update({ 'data.uses.value': uses }); return item.update({"data.uses.value": uses});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -748,7 +753,7 @@ export default class ActorSheet5e extends ActorSheet {
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.items.get(itemId);
return item.rollRecharge(); return item.rollRecharge();
}; }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -769,7 +774,7 @@ export default class ActorSheet5e extends ActorSheet {
} else { } else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`); let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`); let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`)); chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`));
div.append(props); div.append(props);
li.append(div.hide()); li.append(div.hide());
div.slideDown(200); div.slideDown(200);
@ -894,7 +899,7 @@ export default class ActorSheet5e extends ActorSheet {
const label = a.parentElement.querySelector("label"); const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options]; const choices = CONFIG.SW5E[a.dataset.options];
const options = {name: a.dataset.target, title: label.innerText, choices}; const options = {name: a.dataset.target, title: label.innerText, choices};
return new TraitSelector(this.actor, options).render(true) return new TraitSelector(this.actor, options).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -904,7 +909,7 @@ export default class ActorSheet5e extends ActorSheet {
let buttons = super._getHeaderButtons(); let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) { if (this.actor.isPolymorphed) {
buttons.unshift({ buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation', label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation", class: "restore-transformation",
icon: "fas fa-backward", icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm() onclick: () => this.actor.revertOriginalForm()

View file

@ -7,7 +7,6 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacter extends ActorSheet5e { export default class ActorSheet5eCharacter extends ActorSheet5e {
/** /**
* Define default rendering options for the NPC sheet * Define default rendering options for the NPC sheet
* @return {Object} * @return {Object}
@ -45,10 +44,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => { sheetData["multiclassLabels"] = this.actor.itemTypes.class
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ') .map((c) => {
}).join(', '); return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
})
.join(", ");
// Return data for rendering // Return data for rendering
return sheetData; return sheetData;
@ -61,7 +62,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes // Categorize items as inventory, powerbook, features, and classes
const inventory = { const inventory = {
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}}, weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
@ -73,11 +73,23 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}; };
// Partition items by category // Partition items by category
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { let [
items,
powers,
feats,
classes,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details // Item details
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.attunement = { item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: { [CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun", icon: "fa-sun",
@ -92,16 +104,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}[item.data.attunement]; }[item.data.attunement];
// Item usage // Item usage
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); item.isOnCooldown =
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
// Item toggle state // Item toggle state
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Primary Class // Primary Class
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass ); if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types // Classify items into types
if (item.type === "power") arr[1].push(item); if (item.type === "power") arr[1].push(item);
@ -116,7 +130,9 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
else if (item.type === "lightsaberform") arr[10].push(item); else if (item.type === "lightsaberform") arr[10].push(item);
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item); else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
return arr; return arr;
}, [[], [], [], [], [], [], [], [], [], [], []]); },
[[], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); items = this._filterItems(items, this._filters.inventory);
@ -133,21 +149,74 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers); const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter(s => { const nPrepared = powers.filter((s) => {
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
}).length; }).length;
// Organize Features // Organize Features
const features = { const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, classes: {
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, label: "SW5E.ItemTypeClassPl",
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, items: [],
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, hasActions: false,
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, dataset: {type: "class"},
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, isClass: true
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, },
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, classfeatures: {
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, label: "SW5E.ItemTypeClassFeats",
items: [],
hasActions: true,
dataset: {type: "classfeature"},
isClassfeature: true
},
archetype: {
label: "SW5E.ItemTypeArchetype",
items: [],
hasActions: false,
dataset: {type: "archetype"},
isArchetype: true
},
species: {
label: "SW5E.ItemTypeSpecies",
items: [],
hasActions: false,
dataset: {type: "species"},
isSpecies: true
},
background: {
label: "SW5E.ItemTypeBackground",
items: [],
hasActions: false,
dataset: {type: "background"},
isBackground: true
},
fightingstyles: {
label: "SW5E.ItemTypeFightingStylePl",
items: [],
hasActions: false,
dataset: {type: "fightingstyle"},
isFightingstyle: true
},
fightingmasteries: {
label: "SW5E.ItemTypeFightingMasteryPl",
items: [],
hasActions: false,
dataset: {type: "fightingmastery"},
isFightingmastery: true
},
lightsaberforms: {
label: "SW5E.ItemTypeLightsaberFormPl",
items: [],
hasActions: false,
dataset: {type: "lightsaberform"},
isLightsaberform: true
},
active: {
label: "SW5E.FeatureActive",
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}} passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
}; };
for (let f of feats) { for (let f of feats) {
@ -187,8 +256,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} } else {
else {
const isActive = getProperty(item.data, "equipped"); const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : ""; item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -208,11 +276,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if (!this.isEditable) return; if (!this.isEditable) return;
// Item State Toggling // Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this)); html.find(".item-toggle").click(this._onToggleItem.bind(this));
// Short and Long Rest // Short and Long Rest
html.find('.short-rest').click(this._onShortRest.bind(this)); html.find(".short-rest").click(this._onShortRest.bind(this));
html.find('.long-rest').click(this._onLongRest.bind(this)); html.find(".long-rest").click(this._onLongRest.bind(this));
// Rollable sheet actions // Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
@ -281,10 +349,9 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Increment the number of class levels a character instead of creating a new item // Increment the number of class levels a character instead of creating a new item
if (itemData.type === "class") { if (itemData.type === "class") {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0; let priorLevel = cls?.data.data.levels ?? 0;
if (!!cls) { if (!!cls) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);

View file

@ -6,7 +6,6 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPC extends ActorSheet5e { export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
@ -28,27 +27,40 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, weapons: {
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, label: game.i18n.localize("SW5E.AttackPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => { let [powers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); item.isOnCooldown =
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power") arr[0].push(item); if (item.type === "power") arr[0].push(item);
else arr[1].push(item); else arr[1].push(item);
return arr; return arr;
}, [[], []]); },
[[], []]
);
// Apply item filters // Apply item filters
powers = this._filterItems(powers, this._filters.powerbook); powers = this._filterItems(powers, this._filters.powerbook);
@ -63,8 +75,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
else if (item.type === "feat") { else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item); if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} } else features.equipment.items.push(item);
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -72,7 +83,6 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
data.powerbook = powerbook; data.powerbook = powerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @inheritdoc */
@ -95,7 +105,6 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */ /** @override */
async _updateObject(event, formData) { async _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";

View file

@ -25,13 +25,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Creates a new cargo entry for a vehicle Actor. * Creates a new cargo entry for a vehicle Actor.
*/ */
static get newCargo() { static get newCargo() {
return { return {
name: '', name: "",
quantity: 1 quantity: 1
}; };
} }
@ -46,7 +45,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_computeEncumbrance(totalWeight, actorData) { _computeEncumbrance(totalWeight, actorData) {
// Compute currency weight // Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -75,25 +73,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareCrewedItem(item) { _prepareCrewedItem(item) {
// Determine crewed status // Determine crewed status
const isCrewed = item.data.crewed; const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : ''; item.toggleClass = isCrewed ? "active" : "";
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
// Handle crew actions // Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') { if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost; item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === .5) item.cover = '½'; if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === .75) item.cover = '¾'; else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = '—'; else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = '—'; if (item.crew < 1 || item.crew === null) item.crew = "—";
} }
// Prepare vehicle weapons // Prepare vehicle weapons
if (item.type === 'equipment' || item.type === 'weapon') { if (item.type === "equipment" || item.type === "weapon") {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
} }
} }
@ -104,111 +101,125 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
const cargoColumns = [{ const cargoColumns = [
label: game.i18n.localize('SW5E.Quantity'), {
css: 'item-qty', label: game.i18n.localize("SW5E.Quantity"),
property: 'quantity', css: "item-qty",
editable: 'Number' property: "quantity",
}]; editable: "Number"
}
];
const equipmentColumns = [{ const equipmentColumns = [
label: game.i18n.localize('SW5E.Quantity'), {
css: 'item-qty', label: game.i18n.localize("SW5E.Quantity"),
property: 'data.quantity' css: "item-qty",
}, { property: "data.quantity"
label: game.i18n.localize('SW5E.AC'), },
css: 'item-ac', {
property: 'data.armor.value' label: game.i18n.localize("SW5E.AC"),
}, { css: "item-ac",
label: game.i18n.localize('SW5E.HP'), property: "data.armor.value"
css: 'item-hp', },
property: 'data.hp.value', {
editable: 'Number' label: game.i18n.localize("SW5E.HP"),
}, { css: "item-hp",
label: game.i18n.localize('SW5E.Threshold'), property: "data.hp.value",
css: 'item-threshold', editable: "Number"
property: 'threshold' },
}]; {
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = { const features = {
actions: { actions: {
label: game.i18n.localize('SW5E.ActionPl'), label: game.i18n.localize("SW5E.ActionPl"),
items: [], items: [],
crewable: true, crewable: true,
dataset: {type: 'feat', 'activation.type': 'crew'}, dataset: {"type": "feat", "activation.type": "crew"},
columns: [{ columns: [
label: game.i18n.localize('SW5E.VehicleCrew'), {
css: 'item-crew', label: game.i18n.localize("SW5E.VehicleCrew"),
property: 'crew' css: "item-crew",
}, { property: "crew"
label: game.i18n.localize('SW5E.Cover'), },
css: 'item-cover', {
property: 'cover' label: game.i18n.localize("SW5E.Cover"),
}] css: "item-cover",
property: "cover"
}
]
}, },
equipment: { equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'), label: game.i18n.localize("SW5E.ItemTypeEquipment"),
items: [], items: [],
crewable: true, crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'}, dataset: {"type": "equipment", "armor.type": "vehicle"},
columns: equipmentColumns columns: equipmentColumns
}, },
passive: { passive: {
label: game.i18n.localize('SW5E.Features'), label: game.i18n.localize("SW5E.Features"),
items: [], items: [],
dataset: {type: 'feat'} dataset: {type: "feat"}
}, },
reactions: { reactions: {
label: game.i18n.localize('SW5E.ReactionPl'), label: game.i18n.localize("SW5E.ReactionPl"),
items: [], items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'} dataset: {"type": "feat", "activation.type": "reaction"}
}, },
weapons: { weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [], items: [],
crewable: true, crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'}, dataset: {"type": "weapon", "weapon-type": "siege"},
columns: equipmentColumns columns: equipmentColumns
} }
}; };
const cargo = { const cargo = {
crew: { crew: {
label: game.i18n.localize('SW5E.VehicleCrew'), label: game.i18n.localize("SW5E.VehicleCrew"),
items: data.data.cargo.crew, items: data.data.cargo.crew,
css: 'cargo-row crew', css: "cargo-row crew",
editableName: true, editableName: true,
dataset: {type: 'crew'}, dataset: {type: "crew"},
columns: cargoColumns columns: cargoColumns
}, },
passengers: { passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'), label: game.i18n.localize("SW5E.VehiclePassengers"),
items: data.data.cargo.passengers, items: data.data.cargo.passengers,
css: 'cargo-row passengers', css: "cargo-row passengers",
editableName: true, editableName: true,
dataset: {type: 'passengers'}, dataset: {type: "passengers"},
columns: cargoColumns columns: cargoColumns
}, },
cargo: { cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'), label: game.i18n.localize("SW5E.VehicleCargo"),
items: [], items: [],
dataset: {type: 'loot'}, dataset: {type: "loot"},
columns: [{ columns: [
label: game.i18n.localize('SW5E.Quantity'), {
css: 'item-qty', label: game.i18n.localize("SW5E.Quantity"),
property: 'data.quantity', css: "item-qty",
editable: 'Number' property: "data.quantity",
}, { editable: "Number"
label: game.i18n.localize('SW5E.Price'), },
css: 'item-price', {
property: 'data.price', label: game.i18n.localize("SW5E.Price"),
editable: 'Number' css: "item-price",
}, { property: "data.price",
label: game.i18n.localize('SW5E.Weight'), editable: "Number"
css: 'item-weight', },
property: 'data.weight', {
editable: 'Number' label: game.i18n.localize("SW5E.Weight"),
}] css: "item-weight",
property: "data.weight",
editable: "Number"
}
]
} }
}; };
@ -234,8 +245,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
features.equipment.items.push(item); features.equipment.items.push(item);
break; break;
case "feat": case "feat":
if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item); if (!item.data.activation.type || item.data.activation.type === "none")
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item); else features.actions.items.push(item);
break; break;
default: default:
@ -259,21 +271,21 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if (!this.isEditable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this)); html.find(".item-toggle").click(this._onToggleItem.bind(this));
html.find('.item-hp input') html.find(".item-hp input")
.click(evt => evt.target.select()) .click((evt) => evt.target.select())
.change(this._onHPChange.bind(this)); .change(this._onHPChange.bind(this));
html.find('.item:not(.cargo-row) input[data-property]') html.find(".item:not(.cargo-row) input[data-property]")
.click(evt => evt.target.select()) .click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find('.cargo-row input') html.find(".cargo-row input")
.click(evt => evt.target.select()) .click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this)); .change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) { if (this.actor.data.data.attributes.actions.stations) {
html.find('.counter.actions, .counter.action-thresholds').hide(); html.find(".counter.actions, .counter.action-thresholds").hide();
} }
} }
@ -288,9 +300,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) { _onCargoRowChange(event) {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const row = target.closest('.item'); const row = target.closest(".item");
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers'; const property = row.classList.contains("crew") ? "crew" : "passengers";
// Get the cargo entry // Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
@ -298,10 +310,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
if (!entry) return null; if (!entry) return null;
// Update the cargo value // Update the cargo value
const key = target.dataset.property || 'name'; const key = target.dataset.property || "name";
const type = target.dataset.dtype; const type = target.dataset.dtype;
let value = target.value; let value = target.value;
if (type === 'Number') value = Number(value); if (type === "Number") value = Number(value);
entry[key] = value; entry[key] = value;
// Perform the Actor update // Perform the Actor update
@ -318,14 +330,18 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onEditInSheet(event) { _onEditInSheet(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId; const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property; const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype; const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value; let value = event.currentTarget.value;
switch (type) { switch (type) {
case 'Number': value = parseInt(value); break; case "Number":
case 'Boolean': value = value === 'true'; break; value = parseInt(value);
break;
case "Boolean":
value = value === "true";
break;
} }
return item.update({[`${property}`]: value}); return item.update({[`${property}`]: value});
} }
@ -342,7 +358,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const type = target.dataset.type; const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') { if (type === "crew" || type === "passengers") {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo); cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
@ -360,10 +376,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const row = event.currentTarget.closest('.item'); const row = event.currentTarget.closest(".item");
if (row.classList.contains('cargo-row')) { if (row.classList.contains("cargo-row")) {
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers'; const type = row.classList.contains("crew") ? "crew" : "passengers";
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -376,7 +392,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData); return super._onDropItemCreate(itemData);
} }
@ -391,11 +407,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onHPChange(event) { _onHPChange(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId; const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp; event.currentTarget.value = hp;
return item.update({'data.hp.value': hp}); return item.update({"data.hp.value": hp});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -408,9 +424,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId; const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed; const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed}); return item.update({"data.crewed": !crewed});
}
} }
};

View file

@ -39,12 +39,15 @@ export default class AbilityUseDialog extends Dialog {
// Prepare dialog form data // Prepare dialog form data
const data = { const data = {
item: item.data, item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}), title: game.i18n.format("SW5E.AbilityUseHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
name: item.name
}),
note: this._getAbilityUseNote(item.data, uses, recharge), note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false, consumePowerSlot: false,
consumeRecharge: recharges, consumeRecharge: recharges,
consumeResource: !!itemData.consume.target, consumeResource: !!itemData.consume.target,
consumeUses: uses.per && (uses.max > 0), consumeUses: uses.per && uses.max > 0,
canUse: recharges ? recharge.charged : sufficientUses, canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: [] errors: []
@ -65,7 +68,7 @@ export default class AbilityUseDialog extends Dialog {
use: { use: {
icon: `<i class="fas ${icon}"></i>`, icon: `<i class="fas ${icon}"></i>`,
label: label, label: label,
callback: html => { callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form")); const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.toObject()); resolve(fd.toObject());
} }
@ -87,10 +90,9 @@ export default class AbilityUseDialog extends Dialog {
* @private * @private
*/ */
static _getPowerData(actorData, itemData, data) { static _getPowerData(actorData, itemData, data) {
// Determine whether the power may be up-cast // Determine whether the power may be up-cast
const lvl = itemData.level; const lvl = itemData.level;
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots // If can't upcast, return early and don't bother calculating available power slots
if (!consumePowerSlot) { if (!consumePowerSlot) {
@ -106,67 +108,71 @@ export default class AbilityUseDialog extends Dialog {
case "lgt": case "lgt":
case "uni": case "uni":
case "drk": { case "drk": {
powerType = "force" powerType = "force";
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
break; break;
} }
case "tec": { case "tec": {
powerType = "tech" powerType = "tech";
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
break; break;
} }
} }
// eliminate point usage for innate casters // eliminate point usage for innate casters
if (actorData.attributes.powercasting === 'innate') points = 999; if (actorData.attributes.powercasting === "innate") points = 999;
let powerLevels;
let powerLevels
if (powerType === "force") { if (powerType === "force") {
powerLevels = Array.fromRange(10).reduce((arr, i) => { powerLevels = Array.fromRange(10)
.reduce((arr, i) => {
if (i < lvl) return arr; if (i < lvl) return arr;
const label = CONFIG.SW5E.powerLevels[i]; const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power" + i] || {fmax: 0, foverride: null}; const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
let max = parseInt(l.foverride || l.fmax || 0); let max = parseInt(l.foverride || l.fmax || 0);
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
if (max > 0) lmax = i; if (max > 0) lmax = i;
if ((max > 0) && (slots > 0) && (points > i)){ if (max > 0 && slots > 0 && points > i) {
arr.push({ arr.push({
level: i, level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0, canCast: max > 0,
hasSlots: slots > 0 hasSlots: slots > 0
}); });
} }
return arr; return arr;
}, []).filter(sl => sl.level <= lmax); }, [])
.filter((sl) => sl.level <= lmax);
} else if (powerType === "tech") { } else if (powerType === "tech") {
powerLevels = Array.fromRange(10).reduce((arr, i) => { powerLevels = Array.fromRange(10)
.reduce((arr, i) => {
if (i < lvl) return arr; if (i < lvl) return arr;
const label = CONFIG.SW5E.powerLevels[i]; const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null}; const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
let max = parseInt(l.override || l.tmax || 0); let max = parseInt(l.override || l.tmax || 0);
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
if (max > 0) lmax = i; if (max > 0) lmax = i;
if ((max > 0) && (slots > 0) && (points > i)){ if (max > 0 && slots > 0 && points > i) {
arr.push({ arr.push({
level: i, level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0, canCast: max > 0,
hasSlots: slots > 0 hasSlots: slots > 0
}); });
} }
return arr; return arr;
}, []).filter(sl => sl.level <= lmax); }, [])
.filter((sl) => sl.level <= lmax);
} }
const canCast = powerLevels.some((l) => l.hasSlots);
if (!canCast)
const canCast = powerLevels.some(l => l.hasSlots); data.errors.push(
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", { game.i18n.format("SW5E.PowerCastNoSlots", {
level: CONFIG.SW5E.powerLevels[lvl], level: CONFIG.SW5E.powerLevels[lvl],
name: data.item.name name: data.item.name
})); })
);
// Merge power casting data // Merge power casting data
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels}); return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
@ -179,7 +185,6 @@ export default class AbilityUseDialog extends Dialog {
* @private * @private
*/ */
static _getAbilityUseNote(item, uses, recharge) { static _getAbilityUseNote(item, uses, recharge) {
// Zero quantity // Zero quantity
const quantity = item.data.quantity; const quantity = item.data.quantity;
if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
@ -187,8 +192,8 @@ export default class AbilityUseDialog extends Dialog {
// Abilities which use Recharge // Abilities which use Recharge
if (!!recharge.value) { if (!!recharge.value) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
}) });
} }
// Does not use any resource // Does not use any resource

View file

@ -17,7 +17,7 @@ export default class ActorSheetFlags extends DocumentSheet {
/** @override */ /** @override */
get title() { get title() {
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`; return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -40,8 +40,10 @@ export default class ActorSheetFlags extends DocumentSheet {
* @private * @private
*/ */
_getClasses() { _getClasses() {
const classes = this.object.items.filter(i => i.type === "class"); const classes = this.object.items.filter((i) => i.type === "class");
return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => { return classes
.sort((a, b) => a.name.localeCompare(b.name))
.reduce((obj, i) => {
obj[i.id] = i.name; obj[i.id] = i.name;
return obj; return obj;
}, {}); }, {});
@ -63,7 +65,7 @@ export default class ActorSheetFlags extends DocumentSheet {
let flag = foundry.utils.deepClone(v); let flag = foundry.utils.deepClone(v);
flag.type = v.type.name; flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean; flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty('choices'); flag.isSelect = v.hasOwnProperty("choices");
flag.value = getProperty(baseData.flags, `sw5e.${k}`); flag.value = getProperty(baseData.flags, `sw5e.${k}`);
flags[v.section][`flags.sw5e.${k}`] = flag; flags[v.section][`flags.sw5e.${k}`] = flag;
} }
@ -113,7 +115,7 @@ export default class ActorSheetFlags extends DocumentSheet {
let unset = false; let unset = false;
const flags = updateData.flags.sw5e; const flags = updateData.flags.sw5e;
//clone flags to dnd5e for module compatability //clone flags to dnd5e for module compatability
updateData.flags.dnd5e = updateData.flags.sw5e updateData.flags.dnd5e = updateData.flags.sw5e;
for (let [k, v] of Object.entries(flags)) { for (let [k, v] of Object.entries(flags)) {
if ([undefined, null, "", false, 0].includes(v)) { if ([undefined, null, "", false, 0].includes(v)) {
delete flags[k]; delete flags[k];

View file

@ -5,7 +5,6 @@ import Actor5e from "../actor/entity.js";
* @extends {FormApplication} * @extends {FormApplication}
*/ */
export default class ActorTypeConfig extends FormApplication { export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */ /** @inheritdoc */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
@ -32,11 +31,11 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */ /** @override */
getData(options) { getData(options) {
// Get current value or new default // Get current value or new default
let attr = foundry.utils.getProperty(this.object.data.data, 'details.type'); let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
if ( foundry.utils.getType(attr) !== "Object" ) attr = { if (foundry.utils.getType(attr) !== "Object")
value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid", attr = {
value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
subtype: "", subtype: "",
swarm: "", swarm: "",
custom: "" custom: ""
@ -48,7 +47,7 @@ export default class ActorTypeConfig extends FormApplication {
types[k] = { types[k] = {
label: game.i18n.localize(v), label: game.i18n.localize(v),
chosen: attr.value === k chosen: attr.value === k
} };
} }
// Return data for rendering // Return data for rendering
@ -61,12 +60,14 @@ export default class ActorTypeConfig extends FormApplication {
}, },
subtype: attr.subtype, subtype: attr.subtype,
swarm: attr.swarm, swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => { sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
.reverse()
.reduce((obj, e) => {
obj[e[0]] = e[1]; obj[e[0]] = e[1];
return obj; return obj;
}, {}), }, {}),
preview: Actor5e.formatCreatureType(attr) || "" preview: Actor5e.formatCreatureType(attr) || ""
} };
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -74,7 +75,7 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */ /** @override */
async _updateObject(event, formData) { async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData); const typeObject = foundry.utils.expandObject(formData);
return this.object.update({ 'data.details.type': typeObject }); return this.object.update({"data.details.type": typeObject});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -3,7 +3,6 @@
* @implements {DocumentSheet} * @implements {DocumentSheet}
*/ */
export default class ActorHitDiceConfig extends DocumentSheet { export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
@ -26,7 +25,8 @@ export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */ /** @override */
getData(options) { getData(options) {
return { return {
classes: this.object.items.reduce((classes, item) => { classes: this.object.items
.reduce((classes, item) => {
if (item.data.type === "class") { if (item.data.type === "class") {
// Add the appropriate data only if this item is a "class" // Add the appropriate data only if this item is a "class"
classes.push({ classes.push({
@ -35,11 +35,12 @@ export default class ActorHitDiceConfig extends DocumentSheet {
diceDenom: item.data.data.hitDice, diceDenom: item.data.data.hitDice,
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed, currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
maxHitDice: item.data.data.levels, maxHitDice: item.data.data.levels,
canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0 canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
}); });
} }
return classes; return classes;
}, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1))) }, [])
.sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
}; };
} }
@ -50,7 +51,7 @@ export default class ActorHitDiceConfig extends DocumentSheet {
super.activateListeners(html); super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form // Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click(event => { html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget; const button = event.currentTarget;
const current = button.parentElement.querySelector(".current"); const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max"); const max = button.parentElement.querySelector(".max");
@ -67,8 +68,8 @@ export default class ActorHitDiceConfig extends DocumentSheet {
async _updateObject(event, formData) { async _updateObject(event, formData) {
const actorItems = this.object.items; const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({ const classUpdates = Object.entries(formData).map(([id, hd]) => ({
_id: id, "_id": id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd, "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
})); }));
return this.object.updateEmbeddedDocuments("Item", classUpdates); return this.object.updateEmbeddedDocuments("Item", classUpdates);
} }

View file

@ -45,7 +45,7 @@ export default class LongRestDialog extends Dialog {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"), label: game.i18n.localize("SW5E.Rest"),
callback: html => { callback: (html) => {
let newDay = true; let newDay = true;
if (game.settings.get("sw5e", "restVariant") !== "gritty") if (game.settings.get("sw5e", "restVariant") !== "gritty")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
@ -58,7 +58,7 @@ export default class LongRestDialog extends Dialog {
callback: reject callback: reject
} }
}, },
default: 'rest', default: "rest",
close: reject close: reject
}); });
dlg.render(true); dlg.render(true);

View file

@ -3,7 +3,6 @@
* @extends {DocumentSheet} * @extends {DocumentSheet}
*/ */
export default class ActorMovementConfig extends DocumentSheet { export default class ActorMovementConfig extends DocumentSheet {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {

View file

@ -18,11 +18,11 @@ export default class SelectItemsPrompt extends Dialog {
super.activateListeners(html); super.activateListeners(html);
// render the item's sheet if its image is clicked // render the item's sheet if its image is clicked
html.on('click', '.item-image', (event) => { html.on("click", ".item-image", (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId); const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true); item?.sheet.render(true);
}) });
} }
/** /**
@ -33,29 +33,27 @@ export default class SelectItemsPrompt extends Dialog {
* @param {string} options.hint - Localized hint to display at the top of the prompt * @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise<string[]>} - list of item ids which the user has selected * @return {Promise<string[]>} - list of item ids which the user has selected
*/ */
static async create(items, { static async create(items, {hint}) {
hint
}) {
// Render the ability usage template // Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint}); const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
return new Promise((resolve) => { return new Promise((resolve) => {
const dlg = new this(items, { const dlg = new this(items, {
title: game.i18n.localize('SW5E.SelectItemsPromptTitle'), title: game.i18n.localize("SW5E.SelectItemsPromptTitle"),
content: html, content: html,
buttons: { buttons: {
apply: { apply: {
icon: `<i class="fas fa-user-plus"></i>`, icon: `<i class="fas fa-user-plus"></i>`,
label: game.i18n.localize('SW5E.Apply'), label: game.i18n.localize("SW5E.Apply"),
callback: html => { callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form")).toObject(); const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]); const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]);
resolve(selectedIds); resolve(selectedIds);
} }
}, },
cancel: { cancel: {
icon: '<i class="fas fa-forward"></i>', icon: '<i class="fas fa-forward"></i>',
label: game.i18n.localize('SW5E.Skip'), label: game.i18n.localize("SW5E.Skip"),
callback: () => resolve([]) callback: () => resolve([])
} }
}, },

View file

@ -3,7 +3,6 @@
* @extends {DocumentSheet} * @extends {DocumentSheet}
*/ */
export default class ActorSensesConfig extends DocumentSheet { export default class ActorSensesConfig extends DocumentSheet {
/** @inheritdoc */ /** @inheritdoc */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
@ -29,14 +28,15 @@ export default class ActorSensesConfig extends DocumentSheet {
const data = { const data = {
senses: {}, senses: {},
special: senses.special ?? "", special: senses.special ?? "",
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits units: senses.units,
movementUnits: CONFIG.SW5E.movementUnits
}; };
for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) { for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[name]; const v = senses[name];
data.senses[name] = { data.senses[name] = {
label: game.i18n.localize(label), label: game.i18n.localize(label),
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
} };
} }
return data; return data;
} }

View file

@ -59,7 +59,6 @@ export default class ShortRestDialog extends Dialog {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
@ -98,7 +97,7 @@ export default class ShortRestDialog extends Dialog {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"), label: game.i18n.localize("SW5E.Rest"),
callback: html => { callback: (html) => {
let newDay = false; let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty") if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
@ -127,7 +126,9 @@ export default class ShortRestDialog extends Dialog {
* @return {Promise} * @return {Promise}
*/ */
static async longRestDialog({actor} = {}) { static async longRestDialog({actor} = {}) {
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."); console.warn(
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
);
return LongRestDialog.longRestDialog(...arguments); return LongRestDialog.longRestDialog(...arguments);
} }
} }

View file

@ -3,7 +3,6 @@
* @extends {DocumentSheet} * @extends {DocumentSheet}
*/ */
export default class TraitSelector extends DocumentSheet { export default class TraitSelector extends DocumentSheet {
/** @inheritdoc */ /** @inheritdoc */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return foundry.utils.mergeObject(super.defaultOptions, {
@ -38,22 +37,22 @@ export default class TraitSelector extends DocumentSheet {
getData() { getData() {
const attr = foundry.utils.getProperty(this.object.data, this.attribute); const attr = foundry.utils.getProperty(this.object.data, this.attribute);
const o = this.options; const o = this.options;
const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr; const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
const custom = (o.customKey) ? attr[o.customKey] ?? "" : ""; const custom = o.customKey ? attr[o.customKey] ?? "" : "";
// Populate choices // Populate choices
const choices = Object.entries(o.choices).reduce((obj, e) => { const choices = Object.entries(o.choices).reduce((obj, e) => {
let [k, v] = e; let [k, v] = e;
obj[k] = {label: v, chosen: attr ? value.includes(k) : false}; obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
return obj; return obj;
}, {}) }, {});
// Return data // Return data
return { return {
allowCustom: o.allowCustom, allowCustom: o.allowCustom,
choices: choices, choices: choices,
custom: custom custom: custom
} };
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -65,7 +64,7 @@ export default class TraitSelector extends DocumentSheet {
// Obtain choices // Obtain choices
const chosen = []; const chosen = [];
for (let [k, v] of Object.entries(formData)) { for (let [k, v] of Object.entries(formData)) {
if ( (k !== "custom") && v ) chosen.push(k); if (k !== "custom" && v) chosen.push(k);
} }
// Object including custom data // Object including custom data
@ -75,10 +74,10 @@ export default class TraitSelector extends DocumentSheet {
if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom; if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
// Validate the number chosen // Validate the number chosen
if ( o.minimum && (chosen.length < o.minimum) ) { if (o.minimum && chosen.length < o.minimum) {
return ui.notifications.error(`You must choose at least ${o.minimum} options`); return ui.notifications.error(`You must choose at least ${o.minimum} options`);
} }
if ( o.maximum && (chosen.length > o.maximum) ) { if (o.maximum && chosen.length > o.maximum) {
return ui.notifications.error(`You may choose no more than ${o.maximum} options`); return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
} }

View file

@ -8,7 +8,7 @@ export const measureDistances = function(segments, options={}) {
const d = canvas.dimensions; const d = canvas.dimensions;
// Iterate over measured segments // Iterate over measured segments
return segments.map(s => { return segments.map((s) => {
let r = s.ray; let r = s.ray;
// Determine the total distance traveled // Determine the total distance traveled
@ -23,7 +23,7 @@ export const measureDistances = function(segments, options={}) {
// Alternative DMG Movement // Alternative DMG Movement
if (rule === "5105") { if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = (nd10 * 2) + (nd - nd10) + ns; let spaces = nd10 * 2 + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance; return spaces * canvas.dimensions.distance;
} }

View file

@ -157,7 +157,9 @@ export default class CharacterImporter {
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor); this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers( this.addPowers(
sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current), sourceCharacter.attribs
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
.map((e) => e.current),
actor actor
); );

View file

@ -1,4 +1,3 @@
/** /**
* Highlight critical success or failure on d20 rolls * Highlight critical success or failure on d20 rolls
*/ */
@ -11,9 +10,9 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
const d = roll.dice[0]; const d = roll.dice[0];
// Ensure it is an un-modified d20 roll // Ensure it is an un-modified d20 roll
const isD20 = (d.faces === 20) && ( d.values.length === 1 ); const isD20 = d.faces === 20 && d.values.length === 1;
if (!isD20) return; if (!isD20) return;
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure; const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
if (isModifiedRoll) return; if (isModifiedRoll) return;
// Highlight successes and failures // Highlight successes and failures
@ -41,13 +40,13 @@ export const displayChatActionButtons = function(message, html, data) {
// If the user is the message author or the actor owner, proceed // If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor); let actor = game.actors.get(data.message.speaker.actor);
if (actor && actor.isOwner) return; if (actor && actor.isOwner) return;
else if ( game.user.isGM || (data.author.id === game.user.id)) return; else if (game.user.isGM || data.author.id === game.user.id) return;
// Otherwise conceal action buttons except for saving throw // Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]"); const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => { buttons.each((i, btn) => {
if (btn.dataset.action === "save") return; if (btn.dataset.action === "save") return;
btn.style.display = "none" btn.style.display = "none";
}); });
} }
}; };
@ -64,7 +63,7 @@ export const displayChatActionButtons = function(message, html, data) {
* @return {Array} The extended options Array including new context choices * @return {Array} The extended options Array including new context choices
*/ */
export const addChatMessageContextOptions = function (html, options) { export const addChatMessageContextOptions = function (html, options) {
let canApply = li => { let canApply = (li) => {
const message = game.messages.get(li.data("messageId")); const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
}; };
@ -73,25 +72,25 @@ export const addChatMessageContextOptions = function(html, options) {
name: game.i18n.localize("SW5E.ChatContextDamage"), name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>', icon: '<i class="fas fa-user-minus"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, 1) callback: (li) => applyChatCardDamage(li, 1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHealing"), name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>', icon: '<i class="fas fa-user-plus"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, -1) callback: (li) => applyChatCardDamage(li, -1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>', icon: '<i class="fas fa-user-injured"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, 2) callback: (li) => applyChatCardDamage(li, 2)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHalfDamage"), name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>', icon: '<i class="fas fa-user-shield"></i>',
condition: canApply, condition: canApply,
callback: li => applyChatCardDamage(li, 0.5) callback: (li) => applyChatCardDamage(li, 0.5)
} }
); );
return options; return options;
@ -110,10 +109,12 @@ export const addChatMessageContextOptions = function(html, options) {
function applyChatCardDamage(li, multiplier) { function applyChatCardDamage(li, multiplier) {
const message = game.messages.get(li.data("messageId")); const message = game.messages.get(li.data("messageId"));
const roll = message.roll; const roll = message.roll;
return Promise.all(canvas.tokens.controlled.map(t => { return Promise.all(
canvas.tokens.controlled.map((t) => {
const a = t.actor; const a = t.actor;
return a.applyDamage(roll.total, multiplier); return a.applyDamage(roll.total, multiplier);
})); })
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,4 +1 @@
export const ClassFeatures = { export const ClassFeatures = {};
};

View file

@ -1,4 +1,3 @@
/** /**
* Override the default Initiative formula to customize special behaviors of the SW5e system. * Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate * Apply advantage, proficiency, or bonuses where appropriate
@ -18,10 +17,15 @@ export const _getInitiativeFormula = function() {
nd = 2; nd = 2;
mods += "kh"; mods += "kh";
} }
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null]; const parts = [
`${nd}d20${mods}`,
init.mod,
init.prof !== 0 ? init.prof : null,
init.bonus !== 0 ? init.bonus : null
];
// Optionally apply Dexterity tiebreaker // Optionally apply Dexterity tiebreaker
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker"); const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100); if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
return parts.filter(p => p !== null).join(" + "); return parts.filter((p) => p !== null).join(" + ");
}; };

File diff suppressed because it is too large Load diff

View file

@ -23,14 +23,19 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
const constantTerms = []; // Terms that are constant, and their associated operators const constantTerms = []; // Terms that are constant, and their associated operators
let operators = []; // Temporary storage for operators before they are moved to one of the above let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { // For each term for (let term of terms) {
if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array // For each term
else { // Otherwise the term is not an operator if (term instanceof OperatorTerm) operators.push(term);
if (term instanceof DiceTerm) { // If the term is something rollable // If the term is an addition/subtraction operator, push the term into the operators array
else {
// Otherwise the term is not an operator
if (term instanceof DiceTerm) {
// If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well rollableTerms.push(term); // Then place this rollable term into it as well
} // } //
else { // Otherwise, this must be a constant else {
// Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array. constantTerms.push(term); // Then also add this constant term to that array.
} // } //
@ -45,7 +50,7 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
let constantPart = undefined; let constantPart = undefined;
if (constantFormula) { if (constantFormula) {
try { try {
constantPart = Roll.safeEval(constantFormula) constantPart = Roll.safeEval(constantFormula);
} catch (err) { } catch (err) {
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`); console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
} }
@ -111,12 +116,28 @@ function _isUnsupportedTerm(term) {
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled * @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/ */
export async function d20Roll({ export async function d20Roll({
parts=[], data={}, // Roll creation parts = [],
advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization data = {}, // Roll creation
chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration advantage,
chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization disadvantage,
fumble = 1,
critical = 20,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent, // Roll customization
chooseModifier = false,
fastForward = false,
event,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) { } = {}) {
// Handle input arguments // Handle input arguments
const formula = ["1d20"].concat(parts).join(" + "); const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event}); const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
@ -138,14 +159,17 @@ export async function d20Roll({
// Prompt a Dialog to further configure the D20Roll // Prompt a Dialog to further configure the D20Roll
if (!isFF) { if (!isFF) {
const configured = await roll.configureDialog({ const configured = await roll.configureDialog(
{
title, title,
chooseModifier, chooseModifier,
defaultRollMode: defaultRollMode, defaultRollMode: defaultRollMode,
defaultAction: advantageMode, defaultAction: advantageMode,
defaultAbility: data?.item?.ability, defaultAbility: data?.item?.ability,
template template
}, dialogOptions); },
dialogOptions
);
if (configured === null) return null; if (configured === null) return null;
} }
@ -154,7 +178,9 @@ export async function d20Roll({
// Create a Chat Message // Create a Chat Message
if (speaker) { if (speaker) {
console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`); console.warn(
`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker; messageData.speaker = speaker;
} }
if (roll && chatMessage) await roll.toMessage(messageData); if (roll && chatMessage) await roll.toMessage(messageData);
@ -171,7 +197,8 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; else if (disadvantage || event?.ctrlKey || event?.metaKey)
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
return {isFF, advantageMode}; return {isFF, advantageMode};
} }
@ -210,12 +237,25 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled * @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/ */
export async function damageRoll({ export async function damageRoll({
parts=[], data, // Roll creation parts = [],
critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization data, // Roll creation
fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration critical = false,
chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical, // Damage customization
fastForward = false,
event,
allowCritical = true,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) { } = {}) {
// Handle input arguments // Handle input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
@ -233,13 +273,16 @@ export async function damageRoll({
// Prompt a Dialog to further configure the DamageRoll // Prompt a Dialog to further configure the DamageRoll
if (!isFF) { if (!isFF) {
const configured = await roll.configureDialog({ const configured = await roll.configureDialog(
{
title, title,
defaultRollMode: defaultRollMode, defaultRollMode: defaultRollMode,
defaultCritical: isCritical, defaultCritical: isCritical,
template, template,
allowCritical allowCritical
}, dialogOptions); },
dialogOptions
);
if (configured === null) return null; if (configured === null) return null;
} }
@ -248,7 +291,9 @@ export async function damageRoll({
// Create a Chat Message // Create a Chat Message
if (speaker) { if (speaker) {
console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`); console.warn(
`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker; messageData.speaker = speaker;
} }
if (roll && chatMessage) await roll.toMessage(messageData); if (roll && chatMessage) await roll.toMessage(messageData);

View file

@ -16,7 +16,7 @@
export default class D20Roll extends Roll { export default class D20Roll extends Roll {
constructor(formula, data, options) { constructor(formula, data, options) {
super(formula, data, options); super(formula, data, options);
if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) { if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`); throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
} }
this.configureModifiers(); this.configureModifiers();
@ -31,8 +31,8 @@ export default class D20Roll extends Roll {
static ADV_MODE = { static ADV_MODE = {
NORMAL: 0, NORMAL: 0,
ADVANTAGE: 1, ADVANTAGE: 1,
DISADVANTAGE: -1, DISADVANTAGE: -1
} };
/** /**
* The HTML template path used to configure evaluation of this Roll * The HTML template path used to configure evaluation of this Roll
@ -81,13 +81,11 @@ export default class D20Roll extends Roll {
d20.number = this.options.elvenAccuracy ? 3 : 2; d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh"); d20.modifiers.push("kh");
d20.options.advantage = true; d20.options.advantage = true;
} } else if (this.hasDisadvantage) {
else if ( this.hasDisadvantage ) {
d20.number = 2; d20.number = 2;
d20.modifiers.push("kl"); d20.modifiers.push("kl");
d20.options.disadvantage = true; d20.options.disadvantage = true;
} } else d20.number = 1;
else d20.number = 1;
// Assign critical and fumble thresholds // Assign critical and fumble thresholds
if (this.options.critical) d20.options.critical = this.options.critical; if (this.options.critical) d20.options.critical = this.options.critical;
@ -102,7 +100,6 @@ export default class D20Roll extends Roll {
/** @inheritdoc */ /** @inheritdoc */
async toMessage(messageData = {}, options = {}) { async toMessage(messageData = {}, options = {}) {
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play // Evaluate the roll now so we have the results available to determine whether reliable talent came into play
if (!this._evaluated) await this.evaluate({async: true}); if (!this._evaluated) await this.evaluate({async: true});
@ -114,7 +111,7 @@ export default class D20Roll extends Roll {
// Add reliable talent to the d20-term flavor text if it applied // Add reliable talent to the d20-term flavor text if it applied
if (this.options.reliableTalent) { if (this.options.reliableTalent) {
const d20 = this.dice[0]; const d20 = this.dice[0];
const isRT = d20.results.every(r => !r.active || (r.result < 10)); const isRT = d20.results.every((r) => !r.active || r.result < 10);
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`; const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label; if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
} }
@ -140,8 +137,17 @@ export default class D20Roll extends Roll {
* @param {object} options Additional Dialog customization options * @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed * @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/ */
async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) { async configureDialog(
{
title,
defaultRollMode,
defaultAction = D20Roll.ADV_MODE.NORMAL,
chooseModifier = false,
defaultAbility,
template
} = {},
options = {}
) {
// Render the Dialog inner HTML // Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, { const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`, formula: `${this.formula} + @bonus`,
@ -154,32 +160,39 @@ export default class D20Roll extends Roll {
let defaultButton = "normal"; let defaultButton = "normal";
switch (defaultAction) { switch (defaultAction) {
case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break; case D20Roll.ADV_MODE.ADVANTAGE:
case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break; defaultButton = "advantage";
break;
case D20Roll.ADV_MODE.DISADVANTAGE:
defaultButton = "disadvantage";
break;
} }
// Create the Dialog window and await submission of the form // Create the Dialog window and await submission of the form
return new Promise(resolve => { return new Promise((resolve) => {
new Dialog({ new Dialog(
{
title, title,
content, content,
buttons: { buttons: {
advantage: { advantage: {
label: game.i18n.localize("SW5E.Advantage"), label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE)) callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
}, },
normal: { normal: {
label: game.i18n.localize("SW5E.Normal"), label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL)) callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
}, },
disadvantage: { disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"), label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE)) callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
} }
}, },
default: defaultButton, default: defaultButton,
close: () => resolve(null) close: () => resolve(null)
}, options).render(true); },
options
).render(true);
}); });
} }
@ -204,7 +217,7 @@ export default class D20Roll extends Roll {
// Customize the modifier // Customize the modifier
if (form.ability?.value) { if (form.ability?.value) {
const abl = this.data.abilities[form.ability.value]; const abl = this.data.abilities[form.ability.value];
this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod})); this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`; this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
} }

View file

@ -43,7 +43,6 @@ export default class DamageRoll extends Roll {
configureDamage() { configureDamage() {
let flatBonus = 0; let flatBonus = 0;
for (let [i, term] of this.terms.entries()) { for (let [i, term] of this.terms.entries()) {
// Multiply dice terms // Multiply dice terms
if (term instanceof DiceTerm) { if (term instanceof DiceTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
@ -53,33 +52,34 @@ export default class DamageRoll extends Roll {
// Powerful critical - maximize damage and reduce the multiplier by 1 // Powerful critical - maximize damage and reduce the multiplier by 1
if (this.options.powerfulCritical) { if (this.options.powerfulCritical) {
flatBonus += (term.number * term.faces); flatBonus += term.number * term.faces;
cm = Math.max(1, cm - 1); cm = Math.max(1, cm - 1);
} }
// Alter the damage term // Alter the damage term
let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0; let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
term.alter(cm, cb); term.alter(cm, cb);
term.options.critical = true; term.options.critical = true;
} }
} }
// Multiply numeric terms // Multiply numeric terms
else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) { else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber; term.number = term.options.baseNumber;
if (this.isCritical) { if (this.isCritical) {
term.number *= (this.options.criticalMultiplier ?? 2); term.number *= this.options.criticalMultiplier ?? 2;
term.options.critical = true; term.options.critical = true;
} }
} }
} }
// Add powerful critical bonus // Add powerful critical bonus
if ( this.options.powerfulCritical && (flatBonus > 0) ) { if (this.options.powerfulCritical && flatBonus > 0) {
this.terms.push(new OperatorTerm({operator: "+"})); this.terms.push(new OperatorTerm({operator: "+"}));
this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})); this.terms.push(
new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
);
} }
// Re-compile the underlying formula // Re-compile the underlying formula
@ -114,34 +114,39 @@ export default class DamageRoll extends Roll {
* @param {object} options Additional Dialog customization options * @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed * @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/ */
async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) { async configureDialog(
{title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
options = {}
) {
// Render the Dialog inner HTML // Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, { const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`, formula: `${this.formula} + @bonus`,
defaultRollMode, defaultRollMode,
rollModes: CONFIG.Dice.rollModes, rollModes: CONFIG.Dice.rollModes
}); });
// Create the Dialog window and await submission of the form // Create the Dialog window and await submission of the form
return new Promise(resolve => { return new Promise((resolve) => {
new Dialog({ new Dialog(
{
title, title,
content, content,
buttons: { buttons: {
critical: { critical: {
condition: allowCritical, condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"), label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(this._onDialogSubmit(html, true)) callback: (html) => resolve(this._onDialogSubmit(html, true))
}, },
normal: { normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"), label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(this._onDialogSubmit(html, false)) callback: (html) => resolve(this._onDialogSubmit(html, false))
} }
}, },
default: defaultCritical ? "critical" : "normal", default: defaultCritical ? "critical" : "normal",
close: () => resolve(null) close: () => resolve(null)
}, options).render(true); },
options
).render(true);
}); });
} }

15
module/effects.js vendored
View file

@ -10,13 +10,15 @@ export function onManageActiveEffect(event, owner) {
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch (a.dataset.action) { switch (a.dataset.action) {
case "create": case "create":
return owner.createEmbeddedDocuments("ActiveEffect", [{ return owner.createEmbeddedDocuments("ActiveEffect", [
label: game.i18n.localize("SW5E.EffectNew"), {
icon: "icons/svg/aura.svg", "label": game.i18n.localize("SW5E.EffectNew"),
origin: owner.uuid, "icon": "icons/svg/aura.svg",
"origin": owner.uuid,
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
disabled: li.dataset.effectType === "inactive" "disabled": li.dataset.effectType === "inactive"
}]); }
]);
case "edit": case "edit":
return effect.sheet.render(true); return effect.sheet.render(true);
case "delete": case "delete":
@ -32,7 +34,6 @@ export function onManageActiveEffect(event, owner) {
* @return {object} Data for rendering * @return {object} Data for rendering
*/ */
export function prepareActiveEffectCategories(effects) { export function prepareActiveEffectCategories(effects) {
// Define effect header categories // Define effect header categories
const categories = { const categories = {
temporary: { temporary: {

View file

@ -6,7 +6,6 @@ import AbilityUseDialog from "../apps/ability-use-dialog.js";
* @extends {Item} * @extends {Item}
*/ */
export default class Item5e extends Item { export default class Item5e extends Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Item Properties */ /* Item Properties */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -21,7 +20,6 @@ export default class Item5e extends Item {
// Case 1 - defined directly by the item // Case 1 - defined directly by the item
if (itemData.ability) return itemData.ability; if (itemData.ability) return itemData.ability;
// Case 2 - inferred from a parent actor // Case 2 - inferred from a parent actor
else if (this.actor) { else if (this.actor) {
const actorData = this.actor.data.data; const actorData = this.actor.data.data;
@ -29,18 +27,20 @@ export default class Item5e extends Item {
// Powers - Use Actor powercasting modifier based on power school // Powers - Use Actor powercasting modifier based on power school
if (this.data.type === "power") { if (this.data.type === "power") {
switch (this.data.data.school) { switch (this.data.data.school) {
case "lgt": return "wis"; case "lgt":
case "uni": return (actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod) ? "wis" : "cha"; return "wis";
case "drk": return "cha"; case "uni":
case "tec": return "int"; return actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod ? "wis" : "cha";
case "drk":
return "cha";
case "tec":
return "int";
} }
return "none"; return "none";
} }
// Tools - default to Intelligence // Tools - default to Intelligence
else if (this.data.type === "tool") return "int"; else if (this.data.type === "tool") return "int";
// Weapons // Weapons
else if (this.data.type === "weapon") { else if (this.data.type === "weapon") {
const wt = itemData.weaponType; const wt = itemData.weaponType;
@ -53,7 +53,7 @@ export default class Item5e extends Item {
// Finesse weapons - Str or Dex (PHB pg. 147) // Finesse weapons - Str or Dex (PHB pg. 147)
else if (itemData.properties.fin === true) { else if (itemData.properties.fin === true) {
return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str"; return actorData.abilities["dex"].mod >= actorData.abilities["str"].mod ? "dex" : "str";
} }
// Ranged weapons - Dex (PH p.194) // Ranged weapons - Dex (PH p.194)
@ -63,7 +63,7 @@ export default class Item5e extends Item {
} }
// Case 3 - unknown // Case 3 - unknown
return null return null;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -103,7 +103,7 @@ export default class Item5e extends Item {
* @return {boolean} * @return {boolean}
*/ */
get isHealing() { get isHealing() {
return (this.data.data.actionType === "heal") && this.data.data.damage.parts.length; return this.data.data.actionType === "heal" && this.data.data.damage.parts.length;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -136,7 +136,7 @@ export default class Item5e extends Item {
*/ */
get hasAreaTarget() { get hasAreaTarget() {
const target = this.data.data.target; const target = this.data.data.target;
return target && (target.type in CONFIG.SW5E.areaTargetTypes); return target && target.type in CONFIG.SW5E.areaTargetTypes;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -148,7 +148,7 @@ export default class Item5e extends Item {
get hasLimitedUses() { get hasLimitedUses() {
let chg = this.data.data.recharge || {}; let chg = this.data.data.recharge || {};
let uses = this.data.data.uses || {}; let uses = this.data.data.uses || {};
return !!chg.value || (uses.per && (uses.max > 0)); return !!chg.value || (uses.per && uses.max > 0);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -165,7 +165,7 @@ export default class Item5e extends Item {
const itemData = this.data; const itemData = this.data;
const data = itemData.data; const data = itemData.data;
const C = CONFIG.SW5E; const C = CONFIG.SW5E;
const labels = this.labels = {}; const labels = (this.labels = {});
// Classes // Classes
if (itemData.type === "class") { if (itemData.type === "class") {
@ -188,9 +188,12 @@ export default class Item5e extends Item {
// Feat Items // Feat Items
else if (itemData.type === "feat") { else if (itemData.type === "feat") {
const act = data.activation; const act = data.activation;
if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel"); if (act && act.type === C.abilityActivationTypes.legendary)
else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = game.i18n.localize("SW5E.LairActionLabel"); labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel");
else if ( act && act.type ) labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action"); else if (act && act.type === C.abilityActivationTypes.lair)
labels.featType = game.i18n.localize("SW5E.LairActionLabel");
else if (act && act.type)
labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action");
else labels.featType = game.i18n.localize("SW5E.Passive"); else labels.featType = game.i18n.localize("SW5E.Passive");
} }
@ -239,7 +242,6 @@ export default class Item5e extends Item {
// Activated Items // Activated Items
if (data.hasOwnProperty("activation")) { if (data.hasOwnProperty("activation")) {
// Ability Activation Label // Ability Activation Label
let act = data.activation || {}; let act = data.activation || {};
if (act) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" "); if (act) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" ");
@ -268,7 +270,9 @@ export default class Item5e extends Item {
// Recharge Label // Recharge Label
let chg = data.recharge || {}; let chg = data.recharge || {};
labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`; labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${
parseInt(chg.value) < 6 ? "+" : ""
}]`;
} }
// Item Actions // Item Actions
@ -276,8 +280,11 @@ export default class Item5e extends Item {
// Damage // Damage
let dam = data.damage || {}; let dam = data.damage || {};
if (dam.parts) { if (dam.parts) {
labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- "); labels.damage = dam.parts
labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", "); .map((d) => d[0])
.join(" + ")
.replace(/\+ -/g, "- ");
labels.damageTypes = dam.parts.map((d) => C.damageTypes[d[1]]).join(", ");
} }
} }
@ -323,10 +330,10 @@ export default class Item5e extends Item {
const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({ const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({
formula: simplifyRollFormula(damagePart[0], rollData, {constantFirst: false}), formula: simplifyRollFormula(damagePart[0], rollData, {constantFirst: false}),
damageType: damagePart[1], damageType: damagePart[1]
})); }));
this.labels.derivedDamage = derivedDamage this.labels.derivedDamage = derivedDamage;
return derivedDamage; return derivedDamage;
} }
@ -396,7 +403,7 @@ export default class Item5e extends Item {
// Include the item's innate attack bonus as the initial value and label // Include the item's innate attack bonus as the initial value and label
if (itemData.attackBonus) { if (itemData.attackBonus) {
parts.push(itemData.attackBonus) parts.push(itemData.attackBonus);
this.labels.toHit = itemData.attackBonus; this.labels.toHit = itemData.attackBonus;
} }
@ -416,14 +423,15 @@ export default class Item5e extends Item {
if (actorBonus.attack) parts.push(actorBonus.attack); if (actorBonus.attack) parts.push(actorBonus.attack);
// One-time bonus provided by consumed ammunition // One-time bonus provided by consumed ammunition
if ( (itemData.consume?.type === 'ammo') && !!this.actor.items ) { if (itemData.consume?.type === "ammo" && !!this.actor.items) {
const ammoItemData = this.actor.items.get(itemData.consume.target)?.data; const ammoItemData = this.actor.items.get(itemData.consume.target)?.data;
if (ammoItemData) { if (ammoItemData) {
const ammoItemQuantity = ammoItemData.data.quantity; const ammoItemQuantity = ammoItemData.data.quantity;
const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0); const ammoCanBeConsumed = ammoItemQuantity && ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0;
const ammoItemAttackBonus = ammoItemData.data.attackBonus; const ammoItemAttackBonus = ammoItemData.data.attackBonus;
const ammoIsTypeConsumable = (ammoItemData.type === "consumable") && (ammoItemData.data.consumableType === "ammo") const ammoIsTypeConsumable =
ammoItemData.type === "consumable" && ammoItemData.data.consumableType === "ammo";
if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) { if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) {
parts.push("@ammo"); parts.push("@ammo");
rollData["ammo"] = ammoItemAttackBonus; rollData["ammo"] = ammoItemAttackBonus;
@ -432,9 +440,9 @@ export default class Item5e extends Item {
} }
// Condense the resulting attack bonus formula into a simplified label // Condense the resulting attack bonus formula into a simplified label
let toHitLabel = simplifyRollFormula(parts.join('+'), rollData).trim(); let toHitLabel = simplifyRollFormula(parts.join("+"), rollData).trim();
if (toHitLabel.charAt(0) !== '-') { if (toHitLabel.charAt(0) !== "-") {
toHitLabel = '+ ' + toHitLabel toHitLabel = "+ " + toHitLabel;
} }
this.labels.toHit = toHitLabel; this.labels.toHit = toHitLabel;
@ -460,7 +468,7 @@ export default class Item5e extends Item {
max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true}); max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true});
max = Roll.safeEval(max); max = Roll.safeEval(max);
} catch (e) { } catch (e) {
console.error('Problem preparing Max uses for', this.data.name, e); console.error("Problem preparing Max uses for", this.data.name, e);
return; return;
} }
} }
@ -491,7 +499,7 @@ export default class Item5e extends Item {
const isPower = this.type === "power"; // Does the item require a power slot? const isPower = this.type === "power"; // Does the item require a power slot?
// TODO: Possibly Mod this to not consume slots based on class? // TODO: Possibly Mod this to not consume slots based on class?
// We could use this for feats and architypes that let a character cast one slot every rest or so // We could use this for feats and architypes that let a character cast one slot every rest or so
const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); const requirePowerSlot = isPower && id.level > 0 && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
// Define follow-up actions resulting from the item usage // Define follow-up actions resulting from the item usage
let createMeasuredTemplate = hasArea; // Trigger a template creation let createMeasuredTemplate = hasArea; // Trigger a template creation
@ -504,12 +512,16 @@ export default class Item5e extends Item {
if (requirePowerSlot) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`; if (requirePowerSlot) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`;
// Display a configuration dialog to customize the usage // Display a configuration dialog to customize the usage
const needsConfiguration = createMeasuredTemplate || consumeRecharge || (consumeResource && !['simpleB', 'martialB'].includes(id.weaponType)) || consumePowerSlot || (consumeUsage && !['simpleB', 'martialB'].includes(id.weaponType)); const needsConfiguration =
createMeasuredTemplate ||
consumeRecharge ||
(consumeResource && !["simpleB", "martialB"].includes(id.weaponType)) ||
consumePowerSlot ||
(consumeUsage && !["simpleB", "martialB"].includes(id.weaponType));
if (configureDialog && needsConfiguration) { if (configureDialog && needsConfiguration) {
const configuration = await AbilityUseDialog.create(this); const configuration = await AbilityUseDialog.create(this);
if (!configuration) return; if (!configuration) return;
// Determine consumption preferences // Determine consumption preferences
createMeasuredTemplate = Boolean(configuration.placeTemplate); createMeasuredTemplate = Boolean(configuration.placeTemplate);
consumeUsage = Boolean(configuration.consumeUse); consumeUsage = Boolean(configuration.consumeUse);
@ -531,14 +543,20 @@ export default class Item5e extends Item {
} }
// Determine whether the item can be used by testing for resource consumption // Determine whether the item can be used by testing for resource consumption
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerLevel, consumeUsage, consumeQuantity}); const usage = item._getUsageUpdates({
consumeRecharge,
consumeResource,
consumePowerLevel,
consumeUsage,
consumeQuantity
});
if (!usage) return; if (!usage) return;
const {actorUpdates, itemUpdates, resourceUpdates} = usage; const {actorUpdates, itemUpdates, resourceUpdates} = usage;
// Commit pending data updates // Commit pending data updates
if (!foundry.utils.isObjectEmpty(itemUpdates)) await item.update(itemUpdates); if (!foundry.utils.isObjectEmpty(itemUpdates)) await item.update(itemUpdates);
if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete(); if (consumeQuantity && item.data.data.quantity === 0) await item.delete();
if (!foundry.utils.isObjectEmpty(actorUpdates)) await actor.update(actorUpdates); if (!foundry.utils.isObjectEmpty(actorUpdates)) await actor.update(actorUpdates);
if (!foundry.utils.isObjectEmpty(resourceUpdates)) { if (!foundry.utils.isObjectEmpty(resourceUpdates)) {
const resource = actor.items.get(id.consume?.target); const resource = actor.items.get(id.consume?.target);
@ -569,7 +587,6 @@ export default class Item5e extends Item {
* @private * @private
*/ */
_getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) { _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) {
// Reference item data // Reference item data
const id = this.data.data; const id = this.data.data;
const actorUpdates = {}; const actorUpdates = {};
@ -599,7 +616,7 @@ export default class Item5e extends Item {
const fp = this.actor.data.data.attributes.force.points; const fp = this.actor.data.data.attributes.force.points;
const tp = this.actor.data.data.attributes.tech.points; const tp = this.actor.data.data.attributes.tech.points;
const powerCost = id.level + 1; const powerCost = id.level + 1;
const innatePower = this.actor.data.data.attributes.powercasting === 'innate'; const innatePower = this.actor.data.data.attributes.powercasting === "innate";
if (!innatePower) { if (!innatePower) {
switch (id.school) { switch (id.school) {
case "lgt": case "lgt":
@ -608,7 +625,9 @@ export default class Item5e extends Item {
const powers = Number(level?.fvalue ?? 0); const powers = Number(level?.fvalue ?? 0);
if (powers === 0) { if (powers === 0) {
const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); ui.notifications.warn(
game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})
);
return false; return false;
} }
actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0); actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0);
@ -624,7 +643,9 @@ export default class Item5e extends Item {
const powers = Number(level?.tvalue ?? 0); const powers = Number(level?.tvalue ?? 0);
if (powers === 0) { if (powers === 0) {
const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); ui.notifications.warn(
game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})
);
return false; return false;
} }
actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0); actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0);
@ -640,7 +661,6 @@ export default class Item5e extends Item {
} }
} }
// Consume Limited Usage // Consume Limited Usage
if (consumeUsage) { if (consumeUsage) {
const uses = id.uses || {}; const uses = id.uses || {};
@ -655,7 +675,7 @@ export default class Item5e extends Item {
} }
// Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity
if ( consumeQuantity && (!used || (remaining === 0)) ) { if (consumeQuantity && (!used || remaining === 0)) {
const q = Number(id.quantity ?? 1); const q = Number(id.quantity ?? 1);
if (q >= 1) { if (q >= 1) {
used = true; used = true;
@ -694,7 +714,9 @@ export default class Item5e extends Item {
// No consumed target // No consumed target
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
if (!consume.target) { if (!consume.target) {
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})); ui.notifications.warn(
game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})
);
return false; return false;
} }
@ -733,7 +755,9 @@ export default class Item5e extends Item {
// Verify that the required quantity is available // Verify that the required quantity is available
let remaining = quantity - amount; let remaining = quantity - amount;
if (remaining < 0) { if (remaining < 0) {
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel})); ui.notifications.warn(
game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel})
);
return false; return false;
} }
@ -765,7 +789,6 @@ export default class Item5e extends Item {
* the prepared message data (if false) * the prepared message data (if false)
*/ */
async displayCard({rollMode, createMessage = true} = {}) { async displayCard({rollMode, createMessage = true} = {}) {
// Render the chat card template // Render the chat card template
const token = this.actor.token; const token = this.actor.token;
const templateData = { const templateData = {
@ -796,7 +819,7 @@ export default class Item5e extends Item {
}; };
// If the Item was destroyed in the process of displaying its card - embed the item data in the chat message // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) { if (this.data.type === "consumable" && !this.actor.items.has(this.id)) {
chatData.flags["sw5e.itemData"] = this.data; chatData.flags["sw5e.itemData"] = this.data;
} }
@ -830,10 +853,11 @@ export default class Item5e extends Item {
// Equipment properties // Equipment properties
if (data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type)) { if (data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type)) {
if ( data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED ) props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED])); if (data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED)
props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED]));
props.push( props.push(
game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"), game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"), game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient")
); );
} }
@ -848,7 +872,7 @@ export default class Item5e extends Item {
} }
// Filter properties and return // Filter properties and return
data.properties = props.filter(p => !!p); data.properties = props.filter((p) => !!p);
return data; return data;
} }
@ -873,9 +897,7 @@ export default class Item5e extends Item {
* @private * @private
*/ */
_weaponChatData(data, labels, props) { _weaponChatData(data, labels, props) {
props.push( props.push(CONFIG.SW5E.weaponTypes[data.weaponType]);
CONFIG.SW5E.weaponTypes[data.weaponType],
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -899,10 +921,7 @@ export default class Item5e extends Item {
* @private * @private
*/ */
_toolChatData(data, labels, props) { _toolChatData(data, labels, props) {
props.push( props.push(CONFIG.SW5E.abilities[data.ability] || null, CONFIG.SW5E.proficiencyLevels[data.proficient || 0]);
CONFIG.SW5E.abilities[data.ability] || null,
CONFIG.SW5E.proficiencyLevels[data.proficient || 0]
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -926,10 +945,7 @@ export default class Item5e extends Item {
* @private * @private
*/ */
_powerChatData(data, labels, props) { _powerChatData(data, labels, props) {
props.push( props.push(labels.level, labels.components + (labels.materials ? ` (${labels.materials})` : ""));
labels.level,
labels.components + (labels.materials ? ` (${labels.materials})` : "")
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -974,7 +990,7 @@ export default class Item5e extends Item {
if (ammo?.data) { if (ammo?.data) {
const q = ammo.data.data.quantity; const q = ammo.data.data.quantity;
const consumeAmount = consume.amount ?? 0; const consumeAmount = consume.amount ?? 0;
if ( q && (q - consumeAmount >= 0) ) { if (q && q - consumeAmount >= 0) {
this._ammo = ammo; this._ammo = ammo;
title += ` [${ammo.name}]`; title += ` [${ammo.name}]`;
} }
@ -987,7 +1003,8 @@ export default class Item5e extends Item {
} }
// Compose roll options // Compose roll options
const rollConfig = mergeObject({ const rollConfig = mergeObject(
{
parts: parts, parts: parts,
actor: this.actor, actor: this.actor,
data: rollData, data: rollData,
@ -1000,13 +1017,15 @@ export default class Item5e extends Item {
left: window.innerWidth - 710 left: window.innerWidth - 710
}, },
messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id}} messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id}}
}, options); },
options
);
rollConfig.event = options.event; rollConfig.event = options.event;
// Expanded critical hit thresholds // Expanded critical hit thresholds
if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) { if (this.data.type === "weapon" && flags.weaponCriticalThreshold) {
rollConfig.critical = parseInt(flags.weaponCriticalThreshold); rollConfig.critical = parseInt(flags.weaponCriticalThreshold);
} else if (( this.data.type === "power" ) && flags.powerCriticalThreshold) { } else if (this.data.type === "power" && flags.powerCriticalThreshold) {
rollConfig.critical = parseInt(flags.powerCriticalThreshold); rollConfig.critical = parseInt(flags.powerCriticalThreshold);
} }
@ -1015,7 +1034,6 @@ export default class Item5e extends Item {
rollConfig.elvenAccuracy = true; rollConfig.elvenAccuracy = true;
} }
// Apply Halfling Lucky // Apply Halfling Lucky
if (flags.halflingLucky) rollConfig.halflingLucky = true; if (flags.halflingLucky) rollConfig.halflingLucky = true;
@ -1047,7 +1065,7 @@ export default class Item5e extends Item {
const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id}}; const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id}};
// Get roll data // Get roll data
const parts = itemData.damage.parts.map(d => d[0]); const parts = itemData.damage.parts.map((d) => d[0]);
const rollData = this.getRollData(); const rollData = this.getRollData();
if (powerLevel) rollData.item.level = powerLevel; if (powerLevel) rollData.item.level = powerLevel;
@ -1079,12 +1097,12 @@ export default class Item5e extends Item {
} }
// Scale damage from up-casting powers // Scale damage from up-casting powers
if ( (this.data.type === "power") ) { if (this.data.type === "power") {
if ( (itemData.scaling.mode === "atwill") ) { if (itemData.scaling.mode === "atwill") {
const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; const level =
this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData);
} } else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) {
else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) {
const scaling = itemData.scaling.formula; const scaling = itemData.scaling.formula;
this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData); this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData);
} }
@ -1092,7 +1110,7 @@ export default class Item5e extends Item {
// Add damage bonus formula // Add damage bonus formula
const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {}; const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {};
if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) { if (actorBonus.damage && parseInt(actorBonus.damage) !== 0) {
parts.push(actorBonus.damage); parts.push(actorBonus.damage);
} }
@ -1100,9 +1118,9 @@ export default class Item5e extends Item {
const ammoData = this._ammo?.data; const ammoData = this._ammo?.data;
// only add the ammunition damage if the ammution is a consumable with type 'ammo' // only add the ammunition damage if the ammution is a consumable with type 'ammo'
if ( this._ammo && (ammoData.type === "consumable") && (ammoData.data.consumableType === "ammo") ) { if (this._ammo && ammoData.type === "consumable" && ammoData.data.consumableType === "ammo") {
parts.push("@ammo"); parts.push("@ammo");
rollData["ammo"] = ammoData.data.damage.parts.map(p => p[0]).join("+"); rollData["ammo"] = ammoData.data.damage.parts.map((p) => p[0]).join("+");
rollConfig.flavor += ` [${this._ammo.name}]`; rollConfig.flavor += ` [${this._ammo.name}]`;
delete this._ammo; delete this._ammo;
} }
@ -1164,10 +1182,10 @@ export default class Item5e extends Item {
// Attempt to simplify by combining like dice terms // Attempt to simplify by combining like dice terms
let simplified = false; let simplified = false;
if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) { if (s.terms[0] instanceof Die && s.terms.length === 1) {
const d0 = p0.terms[0]; const d0 = p0.terms[0];
const s0 = s.terms[0]; const s0 = s.terms[0];
if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) { if (d0 instanceof Die && d0.faces === s0.faces && d0.modifiers.equals(s0.modifiers)) {
d0.number += s0.number; d0.number += s0.number;
parts[0] = p0.formula; parts[0] = p0.formula;
simplified = true; simplified = true;
@ -1225,10 +1243,14 @@ export default class Item5e extends Item {
const success = roll.total >= parseInt(data.recharge.value); const success = roll.total >= parseInt(data.recharge.value);
// Display a Chat Message // Display a Chat Message
const promises = [roll.toMessage({ const promises = [
flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure")}`, roll.toMessage({
flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(
success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure"
)}`,
speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token}) speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
})]; })
];
// Update the Item data // Update the Item data
if (success) promises.push(this.update({"data.recharge.charged": true})); if (success) promises.push(this.update({"data.recharge.charged": true}));
@ -1258,7 +1280,8 @@ export default class Item5e extends Item {
} }
// Compose the roll data // Compose the roll data
const rollConfig = mergeObject({ const rollConfig = mergeObject(
{
parts: parts, parts: parts,
data: rollData, data: rollData,
title: title, title: title,
@ -1267,13 +1290,15 @@ export default class Item5e extends Item {
dialogOptions: { dialogOptions: {
width: 400, width: 400,
top: options.event ? options.event.clientY - 80 : null, top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710, left: window.innerWidth - 710
}, },
chooseModifier: true, chooseModifier: true,
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false, halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false,
reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"), reliableTalent: this.data.data.proficient >= 1 && this.actor.getFlag("sw5e", "reliableTalent"),
messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}} messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}}
}, options); },
options
);
rollConfig.event = options.event; rollConfig.event = options.event;
// Call the roll helper utility // Call the roll helper utility
@ -1296,13 +1321,15 @@ export default class Item5e extends Item {
if (abl) { if (abl) {
const ability = rollData.abilities[abl]; const ability = rollData.abilities[abl];
if (!ability) { if (!ability) {
console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`); console.warn(
`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`
);
} }
rollData["mod"] = ability?.mod || 0; rollData["mod"] = ability?.mod || 0;
} }
// Include a proficiency score // Include a proficiency score
const prof = ("proficient" in rollData.item) ? (rollData.item.proficient || 0) : 1; const prof = "proficient" in rollData.item ? rollData.item.proficient || 0 : 1;
rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0)); rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0));
return rollData; return rollData;
} }
@ -1312,8 +1339,8 @@ export default class Item5e extends Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
static chatListeners(html) { static chatListeners(html) {
html.on('click', '.card-buttons button', this._onChatCardAction.bind(this)); html.on("click", ".card-buttons button", this._onChatCardAction.bind(this));
html.on('click', '.item-name', this._onChatCardToggleContent.bind(this)); html.on("click", ".item-name", this._onChatCardToggleContent.bind(this));
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -1347,14 +1374,17 @@ export default class Item5e extends Item {
const storedData = message.getFlag("sw5e", "itemData"); const storedData = message.getFlag("sw5e", "itemData");
const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId); const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId);
if (!item) { if (!item) {
return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name})) return ui.notifications.error(
game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name})
);
} }
const powerLevel = parseInt(card.dataset.powerLevel) || null; const powerLevel = parseInt(card.dataset.powerLevel) || null;
// Handle different actions // Handle different actions
switch (action) { switch (action) {
case "attack": case "attack":
await item.rollAttack({event}); break; await item.rollAttack({event});
break;
case "damage": case "damage":
case "versatile": case "versatile":
await item.rollDamage({ await item.rollDamage({
@ -1365,7 +1395,8 @@ export default class Item5e extends Item {
}); });
break; break;
case "formula": case "formula":
await item.rollFormula({event, powerLevel}); break; await item.rollFormula({event, powerLevel});
break;
case "save": case "save":
const targets = this._getChatCardTargets(card); const targets = this._getChatCardTargets(card);
for (let token of targets) { for (let token of targets) {
@ -1374,7 +1405,8 @@ export default class Item5e extends Item {
} }
break; break;
case "toolCheck": case "toolCheck":
await item.rollToolCheck({event}); break; await item.rollToolCheck({event});
break;
case "placeTemplate": case "placeTemplate":
const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); const template = game.sw5e.canvas.AbilityTemplate.fromItem(item);
if (template) template.drawPreview(); if (template) template.drawPreview();
@ -1409,7 +1441,6 @@ export default class Item5e extends Item {
* @private * @private
*/ */
static async _getChatCardActor(card) { static async _getChatCardActor(card) {
// Case 1 - a synthetic actor from a Token // Case 1 - a synthetic actor from a Token
if (card.dataset.tokenId) { if (card.dataset.tokenId) {
const token = await fromUuid(card.dataset.tokenId); const token = await fromUuid(card.dataset.tokenId);
@ -1431,7 +1462,7 @@ export default class Item5e extends Item {
* @private * @private
*/ */
static _getChatCardTargets(card) { static _getChatCardTargets(card) {
let targets = canvas.tokens.controlled.filter(t => !!t.actor); let targets = canvas.tokens.controlled.filter((t) => !!t.actor);
if (!targets.length && game.user.character) targets = targets.concat(game.user.character.getActiveTokens()); if (!targets.length && game.user.character) targets = targets.concat(game.user.character.getActiveTokens());
if (!targets.length) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken")); if (!targets.length) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
return targets; return targets;
@ -1444,7 +1475,7 @@ export default class Item5e extends Item {
/** @inheritdoc */ /** @inheritdoc */
async _preCreate(data, options, user) { async _preCreate(data, options, user) {
await super._preCreate(data, options, user); await super._preCreate(data, options, user);
if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return; if (!this.isEmbedded || this.parent.type === "vehicle") return;
const actorData = this.parent.data; const actorData = this.parent.data;
const isNPC = this.parent.type === "npc"; const isNPC = this.parent.type === "npc";
let updates; let updates;
@ -1470,7 +1501,7 @@ export default class Item5e extends Item {
// The below options are only needed for character classes // The below options are only needed for character classes
if (userId !== game.user.id) return; if (userId !== game.user.id) return;
const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class"); const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class";
if (!isCharacterClass) return; if (!isCharacterClass) return;
// Assign a new primary class // Assign a new primary class
@ -1479,11 +1510,13 @@ export default class Item5e extends Item {
// Prompt to add new class features // Prompt to add new class features
if (options.addFeatures === false) return; if (options.addFeatures === false) return;
this.parent.getClassFeatures({ this.parent
.getClassFeatures({
className: this.name, className: this.name,
archetypeName: this.data.data.archetype, archetypeName: this.data.data.archetype,
level: this.data.data.levels level: this.data.data.levels
}).then(features => { })
.then((features) => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures); return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
}); });
} }
@ -1496,17 +1529,19 @@ export default class Item5e extends Item {
// The below options are only needed for character classes // The below options are only needed for character classes
if (userId !== game.user.id) return; if (userId !== game.user.id) return;
const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class"); const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class";
if (!isCharacterClass) return; if (!isCharacterClass) return;
// Prompt to add new class features // Prompt to add new class features
const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some(k => k in changed.data)); const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some((k) => k in changed.data));
if ( !addFeatures || (options.addFeatures === false) ) return; if (!addFeatures || options.addFeatures === false) return;
this.parent.getClassFeatures({ this.parent
.getClassFeatures({
className: changed.name || this.name, className: changed.name || this.name,
archetypeName: changed.data?.archetype || this.data.data.archetype, archetypeName: changed.data?.archetype || this.data.data.archetype,
level: changed.data?.levels || this.data.data.levels level: changed.data?.levels || this.data.data.levels
}).then(features => { })
.then((features) => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures); return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
}); });
} }
@ -1518,7 +1553,7 @@ export default class Item5e extends Item {
super._onDelete(options, userId); super._onDelete(options, userId);
// Assign a new primary class // Assign a new primary class
if ( this.parent && (this.type === "class") && (userId === game.user.id) ) { if (this.parent && this.type === "class" && userId === game.user.id) {
if (this.id !== this.parent.data.data.details.originalClass) return; if (this.id !== this.parent.data.data.details.originalClass) return;
this.parent._assignPrimaryClass(); this.parent._assignPrimaryClass();
} }
@ -1541,7 +1576,7 @@ export default class Item5e extends Item {
} else { } else {
const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency
const actorArmorProfs = actorData.data.traits?.armorProf?.value || []; const actorArmorProfs = actorData.data.traits?.armorProf?.value || [];
updates["data.proficient"] = (armorProf === true) || actorArmorProfs.includes(armorProf); updates["data.proficient"] = armorProf === true || actorArmorProfs.includes(armorProf);
} }
} }
return updates; return updates;
@ -1577,7 +1612,7 @@ export default class Item5e extends Item {
// TODO: With the changes to make weapon proficiencies more verbose, this may need revising // 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 weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency
const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || []; const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || [];
updates["data.proficient"] = (weaponProf === true) || actorWeaponProfs.includes(weaponProf); updates["data.proficient"] = weaponProf === true || actorWeaponProfs.includes(weaponProf);
} }
} }
return updates; return updates;
@ -1593,10 +1628,10 @@ export default class Item5e extends Item {
* @return {Item5e} The created scroll consumable item * @return {Item5e} The created scroll consumable item
*/ */
static async createScrollFromPower(power) { static async createScrollFromPower(power) {
// Get power data // Get power data
const itemData = power instanceof Item5e ? power.data : power; const itemData = power instanceof Item5e ? power.data : power;
const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data; const {actionType, description, source, activation, duration, target, range, damage, save, level} =
itemData.data;
// Get scroll data // Get scroll data
const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`; const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`;
@ -1606,7 +1641,7 @@ export default class Item5e extends Item {
// Split the scroll description into an intro paragraph and the remaining details // Split the scroll description into an intro paragraph and the remaining details
const scrollDescription = scrollData.data.description.value; const scrollDescription = scrollData.data.description.value;
const pdel = '</p>'; const pdel = "</p>";
const scrollIntroEnd = scrollDescription.indexOf(pdel); const scrollIntroEnd = scrollDescription.indexOf(pdel);
const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length); const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);
const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length); const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);

View file

@ -109,7 +109,7 @@ export default class ItemSheet5e extends ItemSheet {
// Attributes // Attributes
else if (consume.type === "attribute") { else if (consume.type === "attribute") {
const attributes = TokenDocument.getTrackedAttributes(actor.data.data); const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
attributes.bar.forEach(a => a.push("value")); attributes.bar.forEach((a) => a.push("value"));
return attributes.bar.concat(attributes.value).reduce((obj, a) => { return attributes.bar.concat(attributes.value).reduce((obj, a) => {
let k = a.join("."); let k = a.join(".");
obj[k] = k; obj[k] = k;
@ -136,7 +136,10 @@ export default class ItemSheet5e extends ItemSheet {
const label = const label =
uses.per === "charges" uses.per === "charges"
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`; : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
max: uses.max,
per: uses.per
})})`;
obj[i.id] = i.name + label; obj[i.id] = i.name + label;
} }
@ -283,7 +286,10 @@ export default class ItemSheet5e extends ItemSheet {
html.find(".damage-control").click(this._onDamageControl.bind(this)); html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
html.find(".effect-control").click((ev) => { 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."); 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); onManageActiveEffect(ev, this.item);
}); });
} }
@ -337,14 +343,17 @@ export default class ItemSheet5e extends ItemSheet {
}; };
switch (a.dataset.options) { switch (a.dataset.options) {
case 'saves': case "saves":
options.choices = CONFIG.SW5E.abilities; options.choices = CONFIG.SW5E.abilities;
options.valueKey = null; options.valueKey = null;
break; break;
case 'skills': case "skills":
const skills = this.item.data.data.skills; const skills = this.item.data.data.skills;
const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); const choiceSet =
options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0]))); 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; options.maximum = skills.number;
break; break;
} }

View file

@ -1,4 +1,3 @@
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Hotbar Macros */ /* Hotbar Macros */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -17,7 +16,7 @@ export async function create5eMacro(data, slot) {
// Create the macro command // Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`; const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command)); let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
if (!macro) { if (!macro) {
macro = await Macro.create({ macro = await Macro.create({
name: item.name, name: item.name,
@ -46,9 +45,11 @@ export function rollItemMacro(itemName) {
if (!actor) actor = game.actors.get(speaker.actor); if (!actor) actor = game.actors.get(speaker.actor);
// Get matching items // Get matching items
const items = actor ? actor.items.filter(i => i.name === itemName) : []; const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
if (items.length > 1) { 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.`); 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) { } else if (items.length === 0) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
} }

View file

@ -3,7 +3,10 @@
* @return {Promise} A Promise which resolves once the migration is completed * @return {Promise} A Promise which resolves once the migration is completed
*/ */
export const migrateWorld = async function () { 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}); 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 // Migrate World Actors
for await (let a of game.actors.contents) { for await (let a of game.actors.contents) {
@ -43,7 +46,7 @@ export const migrateWorld = async function() {
await s.update(updateData, {enforceTypes: false}); await s.update(updateData, {enforceTypes: false});
// If we do not do this, then synthetic token actors remain in cache // If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData. // with the un-updated actorData.
s.tokens.contents.forEach(t => t._actor = null); s.tokens.contents.forEach((t) => (t._actor = null));
} }
} catch (err) { } catch (err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
@ -102,10 +105,8 @@ export const migrateCompendium = async function(pack) {
// Save the entry, if data was changed // Save the entry, if data was changed
await doc.update(updateData); await doc.update(updateData);
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
} } catch (err) {
// Handle migration failures // Handle migration failures
catch(err) {
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
console.error(err); console.error(err);
} }
@ -147,7 +148,8 @@ export const migrateActorData = async function(actor) {
// Prepared, Equipped, and Proficient for NPC actors // Prepared, Equipped, and Proficient for NPC actors
if (actor.type === "npc") { if (actor.type === "npc") {
if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true; 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, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true; if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
} }
@ -178,14 +180,12 @@ export const migrateActorData = async function(actor) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template * 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 * @param {Object} actorData The data object for an Actor
* @return {Object} The scrubbed Actor data * @return {Object} The scrubbed Actor data
*/ */
function cleanActorData(actorData) { function cleanActorData(actorData) {
// Scrub system data // Scrub system data
const model = game.system.model.Actor[actorData.type]; const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model); actorData.data = filterObject(actorData.data, model);
@ -203,7 +203,6 @@ function cleanActorData(actorData) {
return actorData; return actorData;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -243,22 +242,22 @@ export const migrateActorItemData = async function(item, actor) {
* @return {Object} The updateData to apply * @return {Object} The updateData to apply
*/ */
export const migrateSceneData = async function (scene) { export const migrateSceneData = async function (scene) {
const tokens = await Promise.all(scene.tokens.map(async token => { const tokens = await Promise.all(
scene.tokens.map(async (token) => {
const t = token.toJSON(); const t = token.toJSON();
if (!t.actorId || t.actorLink) { if (!t.actorId || t.actorLink) {
t.actorData = {}; t.actorData = {};
} } else if (!game.actors.has(t.actorId)) {
else if (!game.actors.has(t.actorId)) {
t.actorId = null; t.actorId = null;
t.actorData = {}; t.actorData = {};
} else if (!t.actorLink) { } else if (!t.actorLink) {
const actorData = duplicate(t.actorData); const actorData = duplicate(t.actorData);
actorData.type = token.actor?.type; actorData.type = token.actor?.type;
const update = migrateActorData(actorData); const update = migrateActorData(actorData);
['items', 'effects'].forEach(embeddedName => { ["items", "effects"].forEach((embeddedName) => {
if (!update[embeddedName]?.length) return; if (!update[embeddedName]?.length) return;
const updates = new Map(update[embeddedName].map(u => [u._id, u])); const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
t.actorData[embeddedName].forEach(original => { t.actorData[embeddedName].forEach((original) => {
const update = updates.get(original._id); const update = updates.get(original._id);
if (update) mergeObject(original, update); if (update) mergeObject(original, update);
}); });
@ -268,7 +267,8 @@ export const migrateActorItemData = async function(item, actor) {
mergeObject(t.actorData, update); mergeObject(t.actorData, update);
} }
return t; return t;
})); })
);
return {tokens}; return {tokens};
}; };
@ -284,7 +284,6 @@ export const migrateActorItemData = async function(item, actor) {
* @return {Object} The updated Actor * @return {Object} The updated Actor
*/ */
function _updateNPCData(actor) { function _updateNPCData(actor) {
let actorData = actor.data; let actorData = actor.data;
const updateData = {}; const updateData = {};
// check for flag.core, if not there is no compendium monster so exit // check for flag.core, if not there is no compendium monster so exit
@ -292,13 +291,20 @@ function _updateNPCData(actor) {
if (!hasSource) return actor; if (!hasSource) return actor;
// shortcut out if dataVersion flag is set to 1.2.4 or higher // shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined; 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; 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 // Check to see what the source of NPC is
const sourceId = actor.flags.core.sourceId; const sourceId = actor.flags.core.sourceId;
const coreSource = sourceId.substr(0, sourceId.length - 17); const coreSource = sourceId.substr(0, sourceId.length - 17);
const core_id = sourceId.substr(sourceId.length - 16, 16); const core_id = sourceId.substr(sourceId.length - 16, 16);
if (coreSource === "Compendium.sw5e.monsters") { if (coreSource === "Compendium.sw5e.monsters") {
game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => { game.packs
.get("sw5e.monsters")
.getEntity(core_id)
.then((monster) => {
const monsterData = monster.data.data; const monsterData = monster.data.data;
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
updateData["data.attributes.movement"] = monsterData.attributes.movement; updateData["data.attributes.movement"] = monsterData.attributes.movement;
@ -314,7 +320,9 @@ function _updateNPCData(actor) {
const itemData = i.data; const itemData = i.data;
if (itemData.type === "power") { if (itemData.type === "power") {
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0]; 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); let hasPower = !!actor.items.find(
(item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
);
if (!hasPower) { if (!hasPower) {
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness. // 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)); const newPower = JSON.parse(JSON.stringify(itemData));
@ -331,17 +339,15 @@ function _updateNPCData(actor) {
// set flag to check to see if migration has been done so we don't do it again. // 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"); liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
}) });
} }
//merge object //merge object
actorData = mergeObject(actorData, updateData); actorData = mergeObject(actorData, updateData);
// Return the scrubbed data // Return the scrubbed data
return actor; return actor;
} }
/** /**
* Migrate the actor speed string to movement object * Migrate the actor speed string to movement object
* @private * @private
@ -350,21 +356,21 @@ function _migrateActorMovement(actorData, updateData) {
const ad = actorData.data; const ad = actorData.data;
// Work is needed if old data is present // Work is needed if old data is present
const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value; const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
const hasOld = old !== undefined; const hasOld = old !== undefined;
if (hasOld) { if (hasOld) {
// If new data is not present, migrate the old data // If new data is not present, migrate the old data
const hasNew = ad?.attributes?.movement?.walk !== undefined; const hasNew = ad?.attributes?.movement?.walk !== undefined;
if ( !hasNew && (typeof old === "string") ) { if (!hasNew && typeof old === "string") {
const s = (old || "").split(" "); const s = (old || "").split(" ");
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null; if (s.length > 0)
updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
} }
// Remove the old attribute // Remove the old attribute
updateData["data.attributes.-=speed"] = null; updateData["data.attributes.-=speed"] = null;
} }
return updateData return updateData;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -425,7 +431,7 @@ function _migrateActorPowers(actorData, updateData) {
// Remove the Power DC Bonus // Remove the Power DC Bonus
updateData["data.bonuses.power.-=dc"] = null; updateData["data.bonuses.power.-=dc"] = null;
return updateData return updateData;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -479,11 +485,11 @@ function _migrateActorType(actor, updateData) {
// New default data structure // New default data structure
let data = { let data = {
"value": "", value: "",
"subtype": "", subtype: "",
"swarm": "", swarm: "",
"custom": "" custom: ""
} };
// Specifics // Specifics
// (Some of these have weird names, these need to be addressed individually) // (Some of these have weird names, these need to be addressed individually)
@ -507,13 +513,14 @@ function _migrateActorType(actor, updateData) {
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i; const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
const match = original.trim().match(pattern); const match = original.trim().match(pattern);
if (match) { if (match) {
// Match a known creature type // Match a known creature type
const typeLc = match.groups.type.trim().toLowerCase(); const typeLc = match.groups.type.trim().toLowerCase();
const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => { const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
return (typeLc === k) || return (
(typeLc === game.i18n.localize(v).toLowerCase()) || typeLc === k ||
(typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()); typeLc === game.i18n.localize(v).toLowerCase() ||
typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()
);
}); });
if (typeMatch) data.value = typeMatch[0]; if (typeMatch) data.value = typeMatch[0];
else { else {
@ -527,7 +534,7 @@ function _migrateActorType(actor, updateData) {
if (match.groups.size || isNamedSwarm) { if (match.groups.size || isNamedSwarm) {
const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny"; const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => { const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase()); return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase();
}); });
data.swarm = sizeMatch ? sizeMatch[0] : "tiny"; data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
} else data.swarm = ""; } else data.swarm = "";
@ -560,7 +567,6 @@ function _migrateItemClassPowerCasting(item, updateData) {
}; };
break; break;
case "Engineer": case "Engineer":
updateData["data.powercasting"] = { updateData["data.powercasting"] = {
progression: "engineer", progression: "engineer",
ability: "" ability: ""
@ -608,7 +614,11 @@ async function _migrateItemPower(item, actor, updateData) {
// shortcut out if dataVersion flag is set to 1.2.4 or higher // shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined; 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; 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 // Check to see what the source of Power is
const sourceId = item.flags.core.sourceId; const sourceId = item.flags.core.sourceId;
@ -626,11 +636,10 @@ async function _migrateItemPower(item, actor, updateData) {
const corePowerData = corePower.data; const corePowerData = corePower.data;
// copy Core Power Data over original Power // copy Core Power Data over original Power
updateData["data"] = corePowerData; updateData["data"] = corePowerData;
updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}}; updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}};
return updateData; return updateData;
//game.packs.get(powerType).getEntity(core_id).then(corePower => { //game.packs.get(powerType).getEntity(core_id).then(corePower => {
//}) //})
@ -670,10 +679,10 @@ export async function purgeFlags(pack) {
for (let entity of content) { for (let entity of content) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)}; const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if (pack.entity === "Actor") { if (pack.entity === "Actor") {
update.items = entity.data.items.map(i => { update.items = entity.data.items.map((i) => {
i.flags = cleanFlags(i.flags); i.flags = cleanFlags(i.flags);
return i; return i;
}) });
} }
await pack.updateEntity(update, {recursive: false}); await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`); console.log(`Purged flags from ${entity.name}`);
@ -683,7 +692,6 @@ export async function purgeFlags(pack) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Purge the data model of any inner objects which have been flagged as _deprecated. * Purge the data model of any inner objects which have been flagged as _deprecated.
* @param {object} data The data to clean * @param {object} data The data to clean
@ -695,8 +703,7 @@ export function removeDeprecatedObjects(data) {
if (v._deprecated === true) { if (v._deprecated === true) {
console.log(`Deleting deprecated object key ${k}`); console.log(`Deleting deprecated object key ${k}`);
delete data[k]; delete data[k];
} } else removeDeprecatedObjects(v);
else removeDeprecatedObjects(v);
} }
} }
return data; return data;

View file

@ -5,7 +5,6 @@ import { SW5E } from "../config.js";
* @extends {MeasuredTemplate} * @extends {MeasuredTemplate}
*/ */
export default class AbilityTemplate extends MeasuredTemplate { export default class AbilityTemplate extends MeasuredTemplate {
/** /**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance * 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 * @param {Item5e} item The Item object for which to construct the template
@ -84,7 +83,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
let moveTime = 0; let moveTime = 0;
// Update placement (mouse-move) // Update placement (mouse-move)
handlers.mm = event => { handlers.mm = (event) => {
event.stopPropagation(); event.stopPropagation();
let now = Date.now(); // Apply a 20ms throttle let now = Date.now(); // Apply a 20ms throttle
if (now - moveTime <= 20) return; if (now - moveTime <= 20) return;
@ -96,7 +95,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
}; };
// Cancel the workflow (right-click) // Cancel the workflow (right-click)
handlers.rc = event => { handlers.rc = (event) => {
this.layer.preview.removeChildren(); this.layer.preview.removeChildren();
canvas.stage.off("mousemove", handlers.mm); canvas.stage.off("mousemove", handlers.mm);
canvas.stage.off("mousedown", handlers.lc); canvas.stage.off("mousedown", handlers.lc);
@ -107,7 +106,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
}; };
// Confirm the workflow (left-click) // Confirm the workflow (left-click)
handlers.lc = event => { handlers.lc = (event) => {
handlers.rc(event); handlers.rc(event);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2); const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination); this.data.update(destination);
@ -115,12 +114,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
}; };
// Rotate the template by 3 degree increments (mouse-wheel) // Rotate the template by 3 degree increments (mouse-wheel)
handlers.mw = event => { handlers.mw = (event) => {
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
event.stopPropagation(); event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5; let snap = event.shiftKey ? delta : 5;
this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))}); this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)});
this.refresh(); this.refresh();
}; };

View file

@ -1,5 +1,4 @@
export const registerSystemSettings = function () { export const registerSystemSettings = function () {
/** /**
* Track the system version upon which point a migration was last applied * Track the system version upon which point a migration was last applied
*/ */
@ -22,9 +21,9 @@ export const registerSystemSettings = function() {
default: "normal", default: "normal",
type: String, type: String,
choices: { choices: {
"normal": "SETTINGS.5eRestPHB", normal: "SETTINGS.5eRestPHB",
"gritty": "SETTINGS.5eRestGritty", gritty: "SETTINGS.5eRestGritty",
"epic": "SETTINGS.5eRestEpic", epic: "SETTINGS.5eRestEpic"
} }
}); });
@ -39,11 +38,11 @@ export const registerSystemSettings = function() {
default: "555", default: "555",
type: String, type: String,
choices: { choices: {
"555": "SETTINGS.5eDiagPHB", 555: "SETTINGS.5eDiagPHB",
"5105": "SETTINGS.5eDiagDMG", 5105: "SETTINGS.5eDiagDMG",
"EUCL": "SETTINGS.5eDiagEuclidean", EUCL: "SETTINGS.5eDiagEuclidean"
}, },
onChange: rule => canvas.grid.diagonalRule = rule onChange: (rule) => (canvas.grid.diagonalRule = rule)
}); });
/** /**
@ -79,7 +78,7 @@ export const registerSystemSettings = function() {
scope: "world", scope: "world",
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean
}); });
/** /**
@ -92,7 +91,7 @@ export const registerSystemSettings = function() {
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean,
onChange: s => { onChange: (s) => {
ui.chat.render(); ui.chat.render();
} }
}); });
@ -100,10 +99,10 @@ export const registerSystemSettings = function() {
/** /**
* Option to allow GMs to restrict polymorphing to GMs only. * Option to allow GMs to restrict polymorphing to GMs only.
*/ */
game.settings.register('sw5e', 'allowPolymorphing', { game.settings.register("sw5e", "allowPolymorphing", {
name: 'SETTINGS.5eAllowPolymorphingN', name: "SETTINGS.5eAllowPolymorphingN",
hint: 'SETTINGS.5eAllowPolymorphingL', hint: "SETTINGS.5eAllowPolymorphingL",
scope: 'world', scope: "world",
config: true, config: true,
default: false, default: false,
type: Boolean type: Boolean
@ -112,8 +111,8 @@ export const registerSystemSettings = function() {
/** /**
* Remember last-used polymorph settings. * Remember last-used polymorph settings.
*/ */
game.settings.register('sw5e', 'polymorphSettings', { game.settings.register("sw5e", "polymorphSettings", {
scope: 'client', scope: "client",
default: { default: {
keepPhysical: false, keepPhysical: false,
keepMental: false, keepMental: false,
@ -138,8 +137,8 @@ export const registerSystemSettings = function() {
default: "light", default: "light",
type: String, type: String,
choices: { choices: {
"light": "SETTINGS.SWColorLight", light: "SETTINGS.SWColorLight",
"dark": "SETTINGS.SWColorDark" dark: "SETTINGS.SWColorDark"
} }
}); });
}; };

View file

@ -5,7 +5,6 @@
*/ */
export const preloadHandlebarsTemplates = async function () { export const preloadHandlebarsTemplates = async function () {
return loadTemplates([ return loadTemplates([
// Shared Partials // Shared Partials
"systems/sw5e/templates/actors/parts/active-effects.html", "systems/sw5e/templates/actors/parts/active-effects.html",

View file

@ -3,11 +3,10 @@
* @extends {TokenDocument} * @extends {TokenDocument}
*/ */
export class TokenDocument5e extends TokenDocument { export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */ /** @inheritdoc */
getBarAttribute(...args) { getBarAttribute(...args) {
const data = super.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.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0); data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
} }
@ -15,16 +14,13 @@ export class TokenDocument5e extends TokenDocument {
} }
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Extend the base Token class to implement additional system-specific logic. * Extend the base Token class to implement additional system-specific logic.
* @extends {Token} * @extends {Token}
*/ */
export class Token5e extends Token { export class Token5e extends Token {
/** @inheritdoc */ /** @inheritdoc */
_drawBar(number, bar, data) { _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);
@ -41,7 +37,6 @@ export class Token5e extends Token {
* @private * @private
*/ */
_drawHPBar(number, bar, data) { _drawHPBar(number, bar, data) {
// Extract health data // Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp; let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0); temp = Number(temp || 0);
@ -58,42 +53,50 @@ export class Token5e extends Token {
// Determine colors to use // Determine colors to use
const blk = 0x000000; 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; const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core) // Determine the container size (logic borrowed from core)
const w = this.w; const w = this.w;
let h = Math.max((canvas.dimensions.size / 12), 8); let h = Math.max(canvas.dimensions.size / 12, 8);
if (this.data.height >= 2) h *= 1.6; if (this.data.height >= 2) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2); const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs + 1; const bs1 = bs + 1;
// Overall bar container // Overall bar container
bar.clear() bar.clear();
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3); bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP // Temporary maximum HP
if (tempmax > 0) { if (tempmax > 0) {
const pct = max / effectiveMax; 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 // Maximum HP penalty
else if (tempmax < 0) { else if (tempmax < 0) {
const pct = (max + tempmax) / max; 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 // 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 // Temporary hit points
if (temp > 0) { if (temp > 0) {
bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1); bar.beginFill(c.temp, 1.0)
.lineStyle(0)
.drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
} }
// Set position // Set position
let posY = (number === 0) ? (this.h - h) : 0; let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY); bar.position.set(0, posY);
} }
} }

12
package-lock.json generated
View file

@ -1266,9 +1266,9 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.8", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
}, },
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
@ -3068,9 +3068,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
}, },
"y18n": { "y18n": {
"version": "3.2.1", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
}, },
"yargs": { "yargs": {
"version": "7.1.1", "version": "7.1.1",

135
sw5e.js
View file

@ -18,7 +18,7 @@ import { measureDistances } from "./module/canvas.js";
import Actor5e from "./module/actor/entity.js"; import Actor5e from "./module/actor/entity.js";
import Item5e from "./module/item/entity.js"; import Item5e from "./module/item/entity.js";
import CharacterImporter from "./module/characterImporter.js"; import CharacterImporter from "./module/characterImporter.js";
import { TokenDocument5e, Token5e } from "./module/token.js" import {TokenDocument5e, Token5e} from "./module/token.js";
// Import Applications // Import Applications
import AbilityTemplate from "./module/pixi/ability-template.js"; import AbilityTemplate from "./module/pixi/ability-template.js";
@ -46,9 +46,6 @@ import * as migrations from "./module/migration.js";
/* Foundry VTT Initialization */ /* Foundry VTT Initialization */
/* -------------------------------------------- */ /* -------------------------------------------- */
// Keep on while migrating to Foundry version 0.8
CONFIG.debug.hooks = true;
Hooks.once("init", function () { Hooks.once("init", function () {
console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`); console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
@ -77,7 +74,7 @@ Hooks.once("init", function() {
Actor5e, Actor5e,
Item5e, Item5e,
TokenDocument5e, TokenDocument5e,
Token5e, Token5e
}, },
macros: macros, macros: macros,
migrations: migrations, migrations: migrations,
@ -91,11 +88,7 @@ Hooks.once("init", function() {
CONFIG.Token.documentClass = TokenDocument5e; CONFIG.Token.documentClass = TokenDocument5e;
CONFIG.Token.objectClass = Token5e; CONFIG.Token.objectClass = Token5e;
CONFIG.time.roundTime = 6; CONFIG.time.roundTime = 6;
CONFIG.fontFamilies = [ CONFIG.fontFamilies = ["Engli-Besh", "Open Sans", "Russo One"];
"Engli-Besh",
"Open Sans",
"Russo One"
];
CONFIG.Dice.DamageRoll = dice.DamageRoll; CONFIG.Dice.DamageRoll = dice.DamageRoll;
CONFIG.Dice.D20Roll = dice.D20Roll; CONFIG.Dice.D20Roll = dice.D20Roll;
@ -145,14 +138,37 @@ Hooks.once("init", function() {
// makeDefault: true, // makeDefault: true,
// label: "SW5E.SheetClassStarship" // label: "SW5E.SheetClassStarship"
// }); // });
Actors.registerSheet('sw5e', ActorSheet5eVehicle, { Actors.registerSheet("sw5e", ActorSheet5eVehicle, {
types: ['vehicle'], types: ["vehicle"],
makeDefault: true, makeDefault: true,
label: "SW5E.SheetClassVehicle" label: "SW5E.SheetClassVehicle"
}); });
Items.unregisterSheet("core", ItemSheet); Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("sw5e", ItemSheet5e, { 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'], 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, makeDefault: true,
label: "SW5E.SheetClassItem" label: "SW5E.SheetClassItem"
}); });
@ -161,7 +177,6 @@ Hooks.once("init", function() {
return preloadHandlebarsTemplates(); return preloadHandlebarsTemplates();
}); });
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Foundry VTT Setup */ /* Foundry VTT Setup */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -170,27 +185,73 @@ Hooks.once("init", function() {
* This function runs after game data has been requested and loaded from the servers, so entities exist * This function runs after game data has been requested and loaded from the servers, so entities exist
*/ */
Hooks.once("setup", function () { Hooks.once("setup", function () {
// Localize CONFIG objects once up-front // Localize CONFIG objects once up-front
const toLocalize = [ const toLocalize = [
"abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments", "abilities",
"armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes", "abilityAbbreviations",
"damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages", "abilityActivationTypes",
"limitedUsePeriods", "movementTypes", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills", "abilityConsumptionTypes",
"starshipRolessm", "starshipRolesmed", "starshipRoleslg", "starshipRoleshuge", "starshipRolesgrg", "starshipSkills", "actorSizes",
"powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes", "alignments",
"timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponSizes", "weaponTypes" "armorProficiencies",
"armorPropertiesTypes",
"conditionTypes",
"consumableTypes",
"cover",
"currencies",
"damageResistanceTypes",
"damageTypes",
"distanceUnits",
"equipmentTypes",
"healingTypes",
"itemActionTypes",
"languages",
"limitedUsePeriods",
"movementTypes",
"movementUnits",
"polymorphSettings",
"proficiencyLevels",
"senses",
"skills",
"starshipRolessm",
"starshipRolesmed",
"starshipRoleslg",
"starshipRoleshuge",
"starshipRolesgrg",
"starshipSkills",
"powerComponents",
"powerLevels",
"powerPreparationModes",
"powerScalingModes",
"powerSchools",
"targetTypes",
"timePeriods",
"toolProficiencies",
"weaponProficiencies",
"weaponProperties",
"weaponSizes",
"weaponTypes"
]; ];
// Exclude some from sorting where the default order matters // Exclude some from sorting where the default order matters
const noSort = [ const noSort = [
"abilities", "alignments", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels", "abilities",
"limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes" "alignments",
"currencies",
"distanceUnits",
"movementUnits",
"itemActionTypes",
"proficiencyLevels",
"limitedUsePeriods",
"powerComponents",
"powerLevels",
"powerPreparationModes",
"weaponTypes"
]; ];
// Localize and sort CONFIG objects // Localize and sort CONFIG objects
for (let o of toLocalize) { for (let o of toLocalize) {
const localized = Object.entries(CONFIG.SW5E[o]).map(e => { const localized = Object.entries(CONFIG.SW5E[o]).map((e) => {
return [e[0], game.i18n.localize(e[1])]; return [e[0], game.i18n.localize(e[1])];
}); });
if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1])); if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1]));
@ -202,7 +263,7 @@ Hooks.once("setup", function() {
// add DND5E translation for module compatability // add DND5E translation for module compatability
game.i18n.translations.DND5E = game.i18n.translations.SW5E; game.i18n.translations.DND5E = game.i18n.translations.SW5E;
// console.log(game.settings.get("sw5e", "colorTheme")); // console.log(game.settings.get("sw5e", "colorTheme"));
let theme = game.settings.get("sw5e", "colorTheme") + '-theme'; let theme = game.settings.get("sw5e", "colorTheme") + "-theme";
document.body.classList.add(theme); document.body.classList.add(theme);
}); });
@ -211,7 +272,6 @@ Hooks.once("setup", function() {
* Once the entire VTT framework is initialized, check to see if we should perform a data migration * Once the entire VTT framework is initialized, check to see if we should perform a data migration
*/ */
Hooks.once("ready", function () { Hooks.once("ready", function () {
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to // 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.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
@ -221,8 +281,11 @@ Hooks.once("ready", function() {
const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6"; const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6";
// Check for R1 SW5E versions // Check for R1 SW5E versions
const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6"; const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6";
const COMPATIBLE_MIGRATION_VERSION = 0.80; const COMPATIBLE_MIGRATION_VERSION = 0.8;
const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); const needsMigration =
currentVersion &&
(isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) ||
isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
if (!needsMigration && needsMigration !== "") return; if (!needsMigration && needsMigration !== "") return;
// Perform the migration // Perform the migration
@ -243,13 +306,11 @@ Hooks.on("canvasInit", function() {
SquareGrid.prototype.measureDistances = measureDistances; SquareGrid.prototype.measureDistances = measureDistances;
}); });
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Other Hooks */ /* Other Hooks */
/* -------------------------------------------- */ /* -------------------------------------------- */
Hooks.on("renderChatMessage", (app, html, data) => { Hooks.on("renderChatMessage", (app, html, data) => {
// Display action buttons // Display action buttons
chat.displayChatActionButtons(app, html, data); chat.displayChatActionButtons(app, html, data);
@ -262,7 +323,7 @@ Hooks.on("renderChatMessage", (app, html, data) => {
Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions); Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html)); Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html)); Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions); Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions);
Hooks.on("renderSceneDirectory", (app, html, data) => { Hooks.on("renderSceneDirectory", (app, html, data) => {
//console.log(html.find("header.folder-header")); //console.log(html.find("header.folder-header"));
setFolderBackground(html); setFolderBackground(html);
@ -284,16 +345,14 @@ 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. // FIXME: This helper is needed for the vehicle sheet. It should probably be refactored.
Handlebars.registerHelper('getProperty', function (data, property) { Handlebars.registerHelper("getProperty", function (data, property) {
return getProperty(data, property); return getProperty(data, property);
}); });
function setFolderBackground(html) { function setFolderBackground(html) {
html.find("header.folder-header").each(function () { html.find("header.folder-header").each(function () {
let bgColor = $(this).css("background-color"); let bgColor = $(this).css("background-color");
if(bgColor == undefined) if (bgColor == undefined) bgColor = "rgb(255,255,255)";
bgColor = "rgb(255,255,255)"; $(this).closest("li").css("background-color", bgColor);
$(this).closest('li').css("background-color", bgColor); });
})
} }